diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f160a237a..658ca4c9e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,6 +16,8 @@ jobs:
ruby-version: ['3.0', '3.1', '3.2']
package:
- forest_admin_agent
+ - forest_admin_datasource_active_record
+ - forest_admin_datasource_toolkit
- forest_admin_rails
steps:
@@ -97,6 +99,8 @@ jobs:
with:
coverageLocations: |
${{github.workspace}}/reports/forest_admin_agent/coverage.json:simplecov
+ ${{github.workspace}}/reports/forest_admin_datasource_active_record/coverage.json:simplecov
+ ${{github.workspace}}/reports/forest_admin_datasource_toolkit/coverage.json:simplecov
# ${{github.workspace}}/reports/forest_admin_rails/coverage.json:simplecov
debug: true
diff --git a/.releaserc.js b/.releaserc.js
index 72d49589e..bb16a76df 100644
--- a/.releaserc.js
+++ b/.releaserc.js
@@ -19,6 +19,9 @@ module.exports = {
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' lib/agent_ruby/version.rb; ' +
'sed -i \'s/"version": ".*"/"version": "${nextRelease.version}"/g\' package.json; ' +
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_agent/lib/forest_admin_agent/version.rb; ' +
+ 'sed -i \'s/LIANA_VERSION = ".*"/LIANA_VERSION = "${nextRelease.version}"/g\' packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb; ' +
+ 'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb; '+
+ 'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_rails/lib/forest_admin_rails/version.rb; ',
successCmd:
'mkdir -p $HOME/.gem '+
@@ -26,6 +29,8 @@ module.exports = {
'chmod 0600 $HOME/.gem/credentials '+
'printf -- "---\n:rubygems_api_key: ${env.GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials '+
'( cd packages/forest_admin_agent && gem build && gem push forest_admin_agent-${nextRelease.version}.gem )' +
+ '( cd packages/forest_admin_datasource_toolkit && gem build && gem push forest_admin_datasource_toolkit-${nextRelease.version}.gem )' +
+ '( cd packages/forest_admin_datasource_toolkit && gem build && gem push forest_admin_datasource_toolkit-${nextRelease.version}.gem )' +
'( cd packages/forest_admin_rails && gem build && gem push forest_admin_rails-${nextRelease.version}.gem )' ,
},
],
@@ -36,6 +41,9 @@ module.exports = {
'CHANGELOG.md',
'lib/agent_ruby/version.rb',
'packages/forest_admin_agent/lib/forest_admin_agent/version.rb',
+ 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb',
+ 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb',
+ 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb',
'packages/forest_admin_rails/lib/forest_admin_rails/version.rb',
'package.json'
],
diff --git a/.rubocop.yml b/.rubocop.yml
index a94e31595..8599907af 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -5,18 +5,25 @@ require:
AllCops:
TargetRubyVersion: 3.0
NewCops: enable
+ Exclude:
+ - 'packages/forest_admin_datasource_active_record/spec/dummy/**/*'
+ - 'node_modules/semantic-release-rubygem/**/*'
Gemspec/OrderedDependencies:
Exclude:
- 'packages/forest_admin_agent/forest_admin_agent.gemspec'
- 'packages/forest_admin_rails/forest_admin_rails.gemspec'
- - 'forest_admin_rails.gemspec'
+ - 'packages/forest_admin_datasource_toolkit/forest_admin_datasource_toolkit.gemspec'
+ - 'packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/PercentStringArray:
Exclude:
- 'packages/forest_admin_agent/forest_admin_agent.gemspec'
+ - 'packages/forest_admin_rails/forest_admin_rails.gemspec'
+ - 'packages/forest_admin_datasource_toolkit/forest_admin_datasource_toolkit.gemspec'
+ - 'packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec'
# Offense count: 1
# Configuration parameters: AllowComments.
@@ -29,20 +36,22 @@ Metrics/AbcSize:
- 'packages/forest_admin_agent/lib/forest_admin_agent/auth/auth_manager.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.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_agent/lib/forest_admin_agent/utils/schema/generator_field.rb'
+ - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb'
Metrics/CyclomaticComplexity:
Exclude:
- 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb'
-
-Metrics/MethodLength:
- Max: 20
- Exclude:
- - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb'
+ - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb'
Style/BlockComments:
Exclude:
- 'packages/forest_admin_agent/spec/spec_helper.rb'
- 'packages/forest_admin_rails/spec/spec_helper.rb'
+ - 'packages/forest_admin_datasource_toolkit/spec/spec_helper.rb'
+ - 'packages/forest_admin_datasource_active_record/spec/spec_helper.rb'
# Offense count: 3
# Configuration parameters: AllowedConstants.
@@ -64,7 +73,10 @@ Style/MutableConstant:
Exclude:
- 'lib/agent_ruby/version.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/version.rb'
+ - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb'
- 'packages/forest_admin_rails/lib/forest_admin_rails/version.rb'
+ - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb'
+ - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb'
# Offense count: 38
# This cop supports safe autocorrection (--autocorrect).
@@ -86,6 +98,7 @@ Style/StringLiterals:
- 'packages/forest_admin_agent/lib/forest_admin_agent/version.rb'
- 'packages/forest_admin_agent/spec/forest_admin_agent_spec.rb'
- 'packages/forest_admin_agent/spec/spec_helper.rb'
+ - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb'
- 'packages/forest_admin_rails/forest_admin_rails.gemspec'
- 'packages/forest_admin_rails/Gemfile'
- 'packages/forest_admin_rails/Rakefile'
@@ -95,6 +108,20 @@ Style/StringLiterals:
- 'packages/forest_admin_rails/lib/forest_admin_rails/version.rb'
- 'packages/forest_admin_rails/spec/rails_helper.rb'
- 'packages/forest_admin_rails/spec/spec_helper.rb'
+ - 'packages/forest_admin_datasource_toolkit/forest_admin_datasource_toolkit.gemspec'
+ - 'packages/forest_admin_datasource_toolkit/Gemfile'
+ - 'packages/forest_admin_datasource_toolkit/Rakefile'
+ - 'packages/forest_admin_datasource_toolkit/bin/console'
+ - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit.rb'
+ - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb'
+ - 'packages/forest_admin_datasource_toolkit/spec/spec_helper.rb'
+ - 'packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec'
+ - 'packages/forest_admin_datasource_active_record/Gemfile'
+ - 'packages/forest_admin_datasource_active_record/Rakefile'
+ - 'packages/forest_admin_datasource_active_record/bin/console'
+ - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record.rb'
+ - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb'
+ - 'packages/forest_admin_datasource_active_record/spec/spec_helper.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
@@ -104,6 +131,10 @@ Style/WordArray:
EnforcedStyle: percent
MinSize: 3
+Style/SymbolArray:
+ Exclude:
+ - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_many_to_many_spec.rb'
+
Style/StringLiteralsInInterpolation:
Enabled: true
EnforcedStyle: double_quotes
@@ -112,14 +143,50 @@ Style/RedundantConstantBase:
Exclude:
- 'packages/forest_admin_rails/spec/rails_helper.rb'
-Layout/LineLength:
- Max: 120
+Naming/PredicateName:
+ Exclude:
+ - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb'
+
+Metrics/ParameterLists:
+ Exclude:
+ - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb'
+ - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/column_schema.rb'
+
+Metrics/ModuleLength:
+ CountAsOne: [ 'array', 'hash', 'method_call' ]
+
+Metrics/MethodLength:
+ CountAsOne: ['array', 'hash', 'method_call']
+ Max: 20
+ Exclude:
+ - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.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'
+
+Metrics/BlockLength:
+ Exclude:
+ - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb'
+
+Metrics/PerceivedComplexity:
+ Exclude:
+ - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb'
+
+Metrics/ClassLength:
+ Exclude:
+ - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb'
RSpec/ExampleLength:
+ CountAsOne: [ 'array', 'hash', 'method_call' ]
Max: 20
+
RSpec/MultipleExpectations:
- Max: 5
+ Max: 6
+
+Layout/LineLength:
+ Max: 120
RSpec/MultipleMemoizedHelpers:
Max: 10
diff --git a/packages/forest_admin_agent/Gemfile b/packages/forest_admin_agent/Gemfile
index ffde56103..7e3afca3c 100644
--- a/packages/forest_admin_agent/Gemfile
+++ b/packages/forest_admin_agent/Gemfile
@@ -2,8 +2,11 @@ source "https://rubygems.org"
gemspec
+gem 'forest_admin_datasource_toolkit', path: '../forest_admin_datasource_toolkit'
+
group :development, :test do
- gem "rspec", "~> 3.0"
- gem 'simplecov', "~> 0.22", require: false
- gem 'simplecov_json_formatter', "~> 0.1.4"
+ gem 'rspec', '~> 3.0'
+ gem 'simplecov', '~> 0.22', require: false
+ gem 'simplecov-html', '~> 0.12.3'
+ gem 'simplecov_json_formatter', '~> 0.1.4'
end
diff --git a/packages/forest_admin_agent/forest_admin b/packages/forest_admin_agent/forest_admin
new file mode 100644
index 000000000..b6f1a18ec
--- /dev/null
+++ b/packages/forest_admin_agent/forest_admin
@@ -0,0 +1 @@
+# Logfile created on 2023-09-27 15:44:52 +0200 by logger.rb/v1.5.3
diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_collection.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_collection.rb
new file mode 100644
index 000000000..332f40c01
--- /dev/null
+++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_collection.rb
@@ -0,0 +1,35 @@
+module ForestAdminAgent
+ module Utils
+ module Schema
+ class GeneratorCollection
+ include ForestAdminDatasourceToolkit::Utils
+
+ def self.build_schema(collection)
+ {
+ actions: {},
+ fields: build_fields(collection),
+ icon: nil,
+ integration: nil,
+ isReadOnly: collection.fields.all? { |_k, field| field.type != 'Column' || field.is_read_only },
+ isSearchable: true,
+ isVirtual: false,
+ name: collection.name,
+ onlyForRelationships: false,
+ paginationType: 'page',
+ segments: {}
+ }
+ end
+
+ def self.build_fields(collection)
+ fields = collection.fields.select do |name, _field|
+ !ForestAdminDatasourceToolkit::Utils::Schema.foreign_key?(collection, name) ||
+ ForestAdminDatasourceToolkit::Utils::Schema.primary_key?(collection, name)
+ end
+
+ fields.map { |name, _field| GeneratorField.build_schema(collection, name) }
+ .sort_by { |v| v[:field] }
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb
new file mode 100644
index 000000000..e2d0594bc
--- /dev/null
+++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb
@@ -0,0 +1,182 @@
+module ForestAdminAgent
+ module Utils
+ module Schema
+ class GeneratorField
+ RELATION_MAP = {
+ 'ManyToMany' => 'BelongsToMany',
+ 'ManyToOne' => 'BelongsTo',
+ 'OneToMany' => 'HasMany',
+ 'OneToOne' => 'HasOne'
+ }.freeze
+
+ def self.build_schema(collection, name)
+ type = collection.fields[name].type
+
+ case type
+ when 'Column'
+ schema = build_column_schema(collection, name)
+ when 'ManyToOne', 'OneToMany', 'ManyToMany', 'OneToOne'
+ schema = build_relation_schema(collection, name)
+ end
+
+ schema.sort_by { |k, _v| k }.to_h
+ end
+
+ class << self
+ private
+
+ def build_column_schema(collection, name)
+ column = collection.fields[name]
+ is_foreign_key = ForestAdminDatasourceToolkit::Utils::Schema.foreign_key?(collection, name)
+
+ {
+ defaultValue: column.default_value,
+ enums: column.enum_values.sort,
+ field: name,
+ integration: nil,
+ inverseOf: nil,
+ isFilterable: false, # TODO: FrontendFilterableUtils.isFilterable(),
+ isPrimaryKey: column.is_primary_key,
+
+ # When a column is a foreign key, it is readonly.
+ # This may sound counter-intuitive: it is so that the user don't have two fields which
+ # allow updating the same foreign key in the detail-view form (fk + many to one)
+ isReadOnly: is_foreign_key || column.is_read_only,
+ isRequired: column.validations.any? { |v| v[:operator] == 'Present' },
+ isSortable: column.is_sortable,
+ isVirtual: false,
+ reference: nil,
+ type: convert_column_type(column.column_type),
+ validations: {} # TODO: FrontendValidationUtils.convertValidationList(column),
+ }
+ end
+
+ def convert_column_type(type)
+ return type if type.instance_of? String
+
+ return [convert_column_type(type.first)] if type.instance_of? Array
+
+ {
+ fields: type.map do |key, sub_type|
+ {
+ field: key,
+ type: convert_column_type(sub_type)
+ }
+ end
+ }
+ end
+
+ def foreign_collection_filterable?
+ # TODO: implement FrontendFilterable before
+ true
+ end
+
+ def build_many_to_many_schema(relation, collection, foreign_collection, base_schema)
+ target_name = relation.foreign_key_target
+ target_field = foreign_collection.fields[target_name]
+ through_schema = collection.datasource.collection(relation.through_collection)
+ foreign_schema = through_schema.fields[relation.foreign_key]
+ origin_key = through_schema.fields[relation.origin_key]
+
+ base_schema.merge(
+ {
+ type: [target_field.column_type],
+ defaultValue: nil,
+ isFilterable: false,
+ isPrimaryKey: false,
+ isRequired: false,
+ isReadOnly: origin_key.is_read_only || foreign_schema.is_read_only,
+ isSortable: true,
+ validations: {},
+ reference: "#{foreign_collection.name}.#{target_name}"
+ }
+ )
+ end
+
+ def build_one_to_many_schema(relation, collection, foreign_collection, base_schema)
+ target_name = relation.origin_key_target
+ target_field = collection.fields[target_name]
+ origin_key = foreign_collection.fields[relation.origin_key]
+
+ base_schema.merge(
+ {
+ type: [target_field.column_type],
+ defaultValue: nil,
+ isFilterable: false,
+ isPrimaryKey: false,
+ isRequired: false,
+ isReadOnly: origin_key.is_read_only,
+ isSortable: true,
+ validations: {},
+ reference: "#{foreign_collection.name}.#{target_name}"
+ }
+ )
+ end
+
+ def build_one_to_one_schema(relation, collection, foreign_collection, base_schema)
+ target_field = collection.fields[relation.origin_key_target]
+ key_field = foreign_collection.fields[relation.origin_key]
+
+ base_schema.merge(
+ {
+ type: key_field.column_type,
+ defaultValue: nil,
+ isFilterable: foreign_collection_filterable?,
+ isPrimaryKey: false,
+ isRequired: false,
+ isReadOnly: key_field.is_read_only,
+ isSortable: target_field.is_sortable,
+ validations: {},
+ reference: "#{foreign_collection.name}.#{relation.origin_key_target}"
+ }
+ )
+ end
+
+ def build_many_to_one_schema(relation, collection, foreign_collection, base_schema)
+ key_field = collection.fields[relation.foreign_key]
+
+ base_schema.merge(
+ {
+ type: key_field.column_type,
+ defaultValue: key_field.default_value,
+ isFilterable: foreign_collection_filterable?,
+ isPrimaryKey: false,
+ isRequired: false, # TODO: check with validations
+ isReadOnly: key_field.is_read_only,
+ isSortable: key_field.is_sortable,
+ validations: {}, # TODO: FrontendValidation::convertValidationList(foreignTargetColumn)
+ reference: "#{foreign_collection.name}.#{relation.foreign_key_target}"
+ }
+ )
+ end
+
+ def build_relation_schema(collection, name)
+ relation = collection.fields[name]
+ foreign_collection = collection.datasource.collection(relation.foreign_collection)
+
+ relation_schema = {
+ field: name,
+ enums: nil,
+ integration: nil,
+ isReadOnly: nil,
+ isVirtual: false,
+ inverseOf: nil, # TODO: CollectionUtils::getInverseRelation(collection, name)
+ relationship: RELATION_MAP[relation.type]
+ }
+
+ case relation.type
+ when 'ManyToMany'
+ build_many_to_many_schema(relation, collection, foreign_collection, relation_schema)
+ when 'OneToMany'
+ build_one_to_many_schema(relation, collection, foreign_collection, relation_schema)
+ when 'OneToOne'
+ build_one_to_one_schema(relation, collection, foreign_collection, relation_schema)
+ when 'ManyToOne'
+ build_many_to_one_schema(relation, collection, foreign_collection, relation_schema)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb
new file mode 100644
index 000000000..c6b945566
--- /dev/null
+++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb
@@ -0,0 +1,103 @@
+require 'digest/sha1'
+require 'json'
+
+module ForestAdminAgent
+ module Utils
+ module Schema
+ class SchemaEmitter
+ LIANA_NAME = "agent-ruby"
+
+ LIANA_VERSION = "beta"
+
+ def self.get_serialized_schema(datasource)
+ schema_path = Facades::Container.cache(:schema_path)
+ if Facades::Container.cache(:is_production)
+ schema = if schema_path && File.exist?(schema_path)
+ JSON.parse(File.read(schema_path), { symbolize_names: true })
+ else
+ # TODO: Logger::log('Warn', 'The .forestadmin-schema.json file doesn\'t exist')
+ {
+ meta: meta(Digest::SHA1.hexdigest('')),
+ collections: {}
+ }
+ end
+ else
+ schema = generate(datasource)
+ hash = Digest::SHA1.hexdigest(schema.to_json)
+ schema = {
+ meta: meta(hash),
+ collections: schema
+ }
+
+ File.write(schema_path, JSON.pretty_generate(schema))
+ end
+
+ serialize(schema)
+ end
+
+ class << self
+ private
+
+ def generate(datasource)
+ datasource.collections
+ .map { |_name, collection| GeneratorCollection.build_schema(collection) }
+ .sort_by { |collection| collection[:name] }
+ end
+
+ def meta(hash)
+ {
+ liana: LIANA_NAME,
+ liana_version: LIANA_VERSION,
+ stack: {
+ engine: 'ruby',
+ engine_version: RUBY_VERSION
+ },
+ schemaFileHash: hash
+ }
+ end
+
+ def serialize(schema)
+ data = []
+ included = []
+ schema[:collections].each do |collection|
+ collection_actions = collection[:actions]
+ collection_segments = collection[:segments]
+ collection.delete(:actions)
+ collection.delete(:segments)
+
+ included << get_smart_features_by_collection('actions', collection_actions, with_attributes: true)
+ included << get_smart_features_by_collection('segments', collection_segments, with_attributes: true)
+
+ data.push(
+ {
+ id: collection[:name],
+ type: 'collections',
+ attributes: collection,
+ relationships: {
+ actions: { data: get_smart_features_by_collection('actions', collection_actions) },
+ segments: { data: get_smart_features_by_collection('segments', collection_actions) }
+ }
+ }
+ )
+ end
+
+ {
+ data: data,
+ included: included.reject!(&:empty?),
+ meta: schema[:meta]
+ }
+ end
+
+ def get_smart_features_by_collection(type, data, with_attributes: false)
+ smart_features = []
+ data.each do |value|
+ smart_feature = { id: value[:id], type: type }
+ smart_feature[:attributes] = value if with_attributes
+ smart_features << smart_feature
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_collection_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_collection_spec.rb
new file mode 100644
index 000000000..54ac00fed
--- /dev/null
+++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_collection_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+module ForestAdminAgent
+ module Utils
+ module Schema
+ include ForestAdminDatasourceToolkit
+ include ForestAdminDatasourceToolkit::Schema
+ describe GeneratorCollection do
+ before do
+ @datasource = Datasource.new
+ collection_book = Collection.new(@datasource, 'Book')
+ collection_book.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: '', is_primary_key: true),
+ 'author_id' => ColumnSchema.new(column_type: 'Number'),
+ 'author' => Relations::ManyToOneSchema.new(
+ foreign_key: 'author_id',
+ foreign_key_target: 'id',
+ foreign_collection: 'Person'
+ )
+ }
+ )
+
+ collection_person = Collection.new(@datasource, 'Person')
+ collection_person.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true, is_read_only: true),
+ 'my_self' => Relations::OneToOneSchema.new(
+ origin_key: 'id',
+ origin_key_target: 'id',
+ foreign_collection: 'Person'
+ )
+ }
+ )
+
+ @datasource.add_collection(collection_book)
+ @datasource.add_collection(collection_person)
+ end
+
+ it 'generate schema with readonly false and skipped foreign keys' do
+ schema = described_class.build_schema(@datasource.collection('Book'))
+
+ expect(schema[:isReadOnly]).to be false
+ expect(schema[:fields].size).to eq 2
+ expect(schema[:fields][0][:field]).to eq 'author'
+ expect(schema[:fields][1][:field]).to eq 'id'
+ end
+
+ it 'generate schema with readonly true' do
+ schema = described_class.build_schema(@datasource.collection('Person'))
+
+ expect(schema[:isReadOnly]).to be true
+ end
+
+ it 'have an id, regardless of the fact that it is also a fk on Person collection' do
+ schema = described_class.build_schema(@datasource.collection('Person'))
+
+ expect(schema[:fields][0][:field]).to eq 'id'
+ expect(schema[:fields][0][:isPrimaryKey]).to be true
+ end
+
+ it 'have a one-to-one relationship' do
+ schema = described_class.build_schema(@datasource.collection('Person'))
+
+ expect(schema[:fields][1]).to include(
+ field: 'my_self',
+ isPrimaryKey: false, # Otherwise the UI will try to use it as a column
+ isReadOnly: true, # because the foreignKey that is being used is readonly
+ reference: 'Person.id',
+ relationship: 'HasOne'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_many_to_many_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_many_to_many_spec.rb
new file mode 100644
index 000000000..cddccf621
--- /dev/null
+++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_many_to_many_spec.rb
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+module ForestAdminAgent
+ module Utils
+ module Schema
+ include ForestAdminDatasourceToolkit
+ include ForestAdminDatasourceToolkit::Schema
+ describe GeneratorField do
+ context 'when field is OneToMany relation' do
+ before do
+ @datasource = Datasource.new
+ collection_book = Collection.new(@datasource, 'Book')
+ collection_book.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
+ 'reviewers' => Relations::ManyToManySchema.new(
+ origin_key: 'book_id',
+ origin_key_target: 'id',
+ foreign_collection: 'Person',
+ foreign_key: 'person_id',
+ foreign_key_target: 'id',
+ through_collection: 'BookPerson'
+ )
+ }
+ )
+
+ collection_book_person = Collection.new(@datasource, 'BookPerson')
+ collection_book_person.add_fields(
+ {
+ 'person_id' => ColumnSchema.new(column_type: 'Number', is_read_only: true),
+ 'person' => Relations::ManyToOneSchema.new(
+ foreign_key: 'person_id',
+ foreign_key_target: 'id',
+ foreign_collection: 'Person'
+ ),
+ 'book_id' => ColumnSchema.new(column_type: 'Number', is_read_only: true),
+ 'book' => Relations::ManyToOneSchema.new(
+ foreign_key: 'book_id',
+ foreign_key_target: 'id',
+ foreign_collection: 'Book'
+ )
+ }
+ )
+
+ collection_person = Collection.new(@datasource, 'Person')
+ collection_person.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
+ 'books' => Relations::ManyToManySchema.new(
+ origin_key: 'person_id',
+ origin_key_target: 'id',
+ foreign_collection: 'Book',
+ foreign_key: 'book_id',
+ foreign_key_target: 'id',
+ through_collection: 'BookPerson'
+ )
+ }
+ )
+
+ @datasource.add_collection(collection_book)
+ @datasource.add_collection(collection_book_person)
+ @datasource.add_collection(collection_person)
+ end
+
+ it 'generate relation' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'reviewers')
+
+ expect(schema).to match(
+ {
+ field: 'reviewers',
+ inverseOf: nil,
+ reference: 'Person.id',
+ relationship: 'BelongsToMany',
+ type: ['Number'],
+ defaultValue: nil,
+ enums: nil,
+ integration: nil,
+ isFilterable: false,
+ isPrimaryKey: false,
+ isReadOnly: true,
+ isRequired: false,
+ isSortable: true,
+ isVirtual: false,
+ validations: {}
+ }
+ )
+ end
+
+ it 'sort schema property' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'reviewers')
+
+ expect(schema.keys).to eq(
+ [
+ :defaultValue,
+ :enums,
+ :field,
+ :integration,
+ :inverseOf,
+ :isFilterable,
+ :isPrimaryKey,
+ :isReadOnly,
+ :isRequired,
+ :isSortable,
+ :isVirtual,
+ :reference,
+ :relationship,
+ :type,
+ :validations
+ ]
+ )
+ end
+
+ context 'when the field reference is the primary key' do
+ it 'the many to one relation should not be a primary key' do
+ schema = described_class.build_schema(@datasource.collection('BookPerson'), 'book')
+ expect(schema).to match(
+ {
+ field: 'book',
+ inverseOf: nil,
+ reference: 'Book.id',
+ relationship: 'BelongsTo',
+ type: 'Number',
+ defaultValue: nil,
+ enums: nil,
+ integration: nil,
+ isFilterable: true,
+ isPrimaryKey: false,
+ isReadOnly: true,
+ isRequired: false,
+ isSortable: false,
+ isVirtual: false,
+ validations: {}
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_one_to_many_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_one_to_many_spec.rb
new file mode 100644
index 000000000..e725dada3
--- /dev/null
+++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_one_to_many_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+module ForestAdminAgent
+ module Utils
+ module Schema
+ include ForestAdminDatasourceToolkit
+ include ForestAdminDatasourceToolkit::Schema
+ describe GeneratorField do
+ context 'when field is OneToMany relation' do
+ before do
+ @datasource = Datasource.new
+ collection_book = Collection.new(@datasource, 'Book')
+ collection_book.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
+ 'author_id' => ColumnSchema.new(column_type: 'Number', is_read_only: true, is_sortable: true),
+ 'author' => Relations::ManyToOneSchema.new(
+ foreign_key: 'author_id',
+ foreign_key_target: 'id',
+ foreign_collection: 'Person'
+ )
+ }
+ )
+
+ collection_person = Collection.new(@datasource, 'Person')
+ collection_person.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
+ 'written_books' => Relations::OneToManySchema.new(
+ origin_key: 'author_id',
+ origin_key_target: 'id',
+ foreign_collection: 'Book'
+ )
+ }
+ )
+
+ @datasource.add_collection(collection_book)
+ @datasource.add_collection(collection_person)
+ end
+
+ it 'generate relation' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'author')
+
+ expect(schema).to match(
+ {
+ field: 'author',
+ inverseOf: nil,
+ reference: 'Person.id',
+ relationship: 'BelongsTo',
+ type: 'Number',
+ defaultValue: nil,
+ enums: nil,
+ integration: nil,
+ isFilterable: true,
+ isPrimaryKey: false,
+ isReadOnly: true,
+ isRequired: false,
+ isSortable: true,
+ isVirtual: false,
+ validations: {}
+ }
+ )
+ end
+
+ it 'generate inverse relation' do
+ schema = described_class.build_schema(@datasource.collection('Person'), 'written_books')
+
+ expect(schema).to match(
+ {
+ field: 'written_books',
+ inverseOf: nil,
+ reference: 'Book.id',
+ relationship: 'HasMany',
+ type: ['Number'],
+ isSortable: true,
+ defaultValue: nil,
+ enums: nil,
+ integration: nil,
+ isFilterable: false,
+ isPrimaryKey: false,
+ isReadOnly: true,
+ isRequired: false,
+ isVirtual: false,
+ validations: {}
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_one_to_one_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_one_to_one_spec.rb
new file mode 100644
index 000000000..aed8563f4
--- /dev/null
+++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_one_to_one_spec.rb
@@ -0,0 +1,95 @@
+require 'spec_helper'
+
+module ForestAdminAgent
+ module Utils
+ module Schema
+ include ForestAdminDatasourceToolkit
+ include ForestAdminDatasourceToolkit::Schema
+ describe GeneratorField do
+ context 'when field is OneToOne relation' do
+ before do
+ @datasource = Datasource.new
+ collection_book = Collection.new(@datasource, 'Book')
+ collection_book.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
+ 'author_id' => ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: true),
+ 'author' => Relations::ManyToOneSchema.new(
+ foreign_key: 'author_id',
+ foreign_key_target: 'id',
+ foreign_collection: 'Person'
+ )
+ }
+ )
+
+ collection_person = Collection.new(@datasource, 'Person')
+ collection_person.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
+ 'book' => Relations::OneToOneSchema.new(
+ origin_key: 'author_id',
+ origin_key_target: 'id',
+ foreign_collection: 'Book'
+ )
+ }
+ )
+
+ @datasource.add_collection(collection_book)
+ @datasource.add_collection(collection_person)
+ end
+
+ it 'generate relation' do
+ schema = described_class.build_schema(@datasource.collection('Person'), 'book')
+
+ expect(schema).to match(
+ {
+ field: 'book',
+ inverseOf: nil,
+
+ reference: 'Book.id',
+ relationship: 'HasOne',
+ type: 'String',
+
+ defaultValue: nil,
+ enums: nil,
+ integration: nil,
+ isFilterable: true,
+ isPrimaryKey: false,
+ isReadOnly: true,
+ isRequired: false,
+ isSortable: false,
+ isVirtual: false,
+ validations: {}
+ }
+ )
+ end
+
+ it 'generate inverse relation' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'author')
+
+ expect(schema).to match(
+ {
+ field: 'author',
+ inverseOf: nil,
+ reference: 'Person.id',
+ relationship: 'BelongsTo',
+ type: 'String',
+ isSortable: true,
+
+ defaultValue: nil,
+ enums: nil,
+ integration: nil,
+ isFilterable: true,
+ isPrimaryKey: false,
+ isReadOnly: true,
+ isRequired: false,
+ isVirtual: false,
+ validations: {}
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_spec.rb
new file mode 100644
index 000000000..5ca22f164
--- /dev/null
+++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+module ForestAdminAgent
+ module Utils
+ module Schema
+ include ForestAdminDatasourceToolkit
+ include ForestAdminDatasourceToolkit::Schema
+ describe GeneratorField do
+ before do
+ @datasource = Datasource.new
+ collection_book = Collection.new(@datasource, 'Book')
+ collection_book.add_fields(
+ {
+ 'isbn' => ColumnSchema.new(
+ column_type: 'String',
+ is_primary_key: true,
+ filter_operators: %w[Equal NotEqual Present],
+ is_sortable: true,
+ is_read_only: true,
+ validations: [{ operator: 'Present' }]
+ ),
+ 'origin_key' => ColumnSchema.new(
+ column_type: 'Number',
+ is_primary_key: false,
+ is_sortable: false,
+ is_read_only: false
+ ),
+ 'composite' => ColumnSchema.new(
+ column_type: { firstname: 'String', lastname: 'String' }
+ ),
+ 'array_of_composite' => ColumnSchema.new(
+ column_type: [{ firstname: 'String', lastname: 'String' }]
+ )
+ }
+ )
+
+ @datasource.add_collection(collection_book)
+ end
+
+ it 'generate the proper schema for the isbn field' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'isbn')
+
+ expect(schema).to match(
+ {
+ field: 'isbn',
+ type: 'String',
+ isSortable: true,
+ inverseOf: nil,
+ defaultValue: nil,
+ enums: [],
+ integration: nil,
+ isFilterable: false,
+ isPrimaryKey: true,
+ isReadOnly: true,
+ isRequired: true,
+ isVirtual: false,
+ reference: nil,
+ validations: {}
+ }
+ )
+ end
+
+ it 'generate the proper schema for the other field' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'origin_key')
+
+ expect(schema).to match(
+ {
+ field: 'origin_key',
+ type: 'Number',
+ isSortable: false,
+ inverseOf: nil,
+ defaultValue: nil,
+ enums: [],
+ integration: nil,
+ isFilterable: false,
+ isPrimaryKey: false,
+ isReadOnly: false,
+ isRequired: false,
+ isVirtual: false,
+ reference: nil,
+ validations: {}
+ }
+ )
+ end
+
+ it 'generate the proper schema for composite types' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'composite')
+
+ expect(schema[:type]).to match(
+ {
+ fields: [
+ { field: :firstname, type: 'String' },
+ { field: :lastname, type: 'String' }
+ ]
+ }
+ )
+ end
+
+ it 'generate the proper schema for array of composites types' do
+ schema = described_class.build_schema(@datasource.collection('Book'), 'array_of_composite')
+
+ expect(schema[:type]).to match(
+ [
+ {
+ fields: [
+ { field: :firstname, type: 'String' },
+ { field: :lastname, type: 'String' }
+ ]
+ }
+ ]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/schema_emitter_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/schema_emitter_spec.rb
new file mode 100644
index 000000000..752df64a0
--- /dev/null
+++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/schema_emitter_spec.rb
@@ -0,0 +1,135 @@
+require 'spec_helper'
+require 'digest/sha1'
+require 'json'
+
+module ForestAdminAgent
+ module Utils
+ module Schema
+ include ForestAdminDatasourceToolkit
+ include ForestAdminDatasourceToolkit::Schema
+
+ describe SchemaEmitter do
+ before do
+ @datasource = Datasource.new
+ collection_book = Collection.new(@datasource, 'Book')
+ collection_book.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: '', is_primary_key: true),
+ 'author_id' => ColumnSchema.new(column_type: 'Number'),
+ 'author' => Relations::ManyToOneSchema.new(
+ foreign_key: 'author_id',
+ foreign_key_target: 'id',
+ foreign_collection: 'Person'
+ )
+ }
+ )
+ collection_person = Collection.new(@datasource, 'Person')
+ collection_person.add_fields(
+ {
+ 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true),
+ 'book' => Relations::OneToOneSchema.new(
+ origin_key: 'author_id',
+ origin_key_target: 'id',
+ foreign_collection: 'Book'
+ )
+ }
+ )
+
+ @datasource.add_collection(collection_book)
+ @datasource.add_collection(collection_person)
+ end
+
+ context 'when env is not production' do
+ before do
+ cache = ForestAdminAgent::Builder::AgentFactory.instance.container.resolve(:cache)
+ config = cache.get('config')
+ cache.clear 'config'
+ config[:is_production] = false
+ cache.get 'config' do
+ config
+ end
+ end
+
+ it 'generate serialized schema' do
+ schema = described_class.get_serialized_schema(@datasource)
+
+ expect(schema).to include(:data, :included, :meta)
+ expect(schema[:data][0].keys).to eq(%i[id type attributes relationships])
+ expect(schema[:data][0][:attributes].keys).to eq(
+ %i[fields icon integration isReadOnly isSearchable isVirtual name onlyForRelationships paginationType]
+ )
+ expect(schema[:data][0][:relationships].keys).to eq(%i[actions segments])
+ expect(schema[:data][0][:relationships][:actions].keys).to eq(%i[data])
+ expect(schema[:data][0][:relationships][:segments].keys).to eq(%i[data])
+ end
+
+ it 'generate the schema json' do
+ schema_path = Facades::Container.cache(:schema_path)
+ FileUtils.rm_f schema_path
+ described_class.get_serialized_schema(@datasource)
+
+ expect(File.exist?(schema_path)).to be true
+ end
+ end
+
+ context 'when env is production' do
+ before do
+ cache = ForestAdminAgent::Builder::AgentFactory.instance.container.resolve(:cache)
+ config = cache.get('config')
+ cache.clear 'config'
+ config[:is_production] = true
+ cache.get 'config' do
+ config
+ end
+ end
+
+ it 'generate empty serialized schema when json does not exist' do
+ schema_path = Facades::Container.cache(:schema_path)
+ FileUtils.rm_f schema_path
+ schema = described_class.get_serialized_schema(@datasource)
+
+ expect(schema).to eq(
+ {
+ data: [],
+ included: nil,
+ meta: {
+ liana: described_class::LIANA_NAME,
+ liana_version: described_class::LIANA_VERSION,
+ stack: {
+ engine: 'ruby',
+ engine_version: RUBY_VERSION
+ },
+ schemaFileHash: Digest::SHA1.hexdigest('')
+ }
+ }
+ )
+ end
+
+ it 'generate serialized schema with the existing json' do
+ schema_path = Facades::Container.cache(:schema_path)
+ FileUtils.rm_f schema_path
+ json_schema = {
+ meta: {
+ liana: 'agent-ruby',
+ liana_version: 'beta',
+ stack: { engine: 'ruby', engine_version: '3.2.0' },
+ schemaFileHash: 'c3af464045ea4b3a2ddefd986a19b746680e1177'
+ },
+ collections: []
+ }
+ File.write(schema_path, JSON.pretty_generate(json_schema))
+ schema = described_class.get_serialized_schema(@datasource)
+
+ expect(schema).to eq(
+ {
+ data: [],
+ included: nil,
+ meta: json_schema[:meta]
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_agent/spec/spec_helper.rb b/packages/forest_admin_agent/spec/spec_helper.rb
index 15d1fa68a..4539ebced 100644
--- a/packages/forest_admin_agent/spec/spec_helper.rb
+++ b/packages/forest_admin_agent/spec/spec_helper.rb
@@ -1,9 +1,12 @@
require 'lightly'
require 'simplecov'
require 'simplecov_json_formatter'
+require 'simplecov-html'
require 'forest_admin_agent'
+require 'forest_admin_datasource_toolkit'
+require 'singleton'
-SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter
+SimpleCov.formatters = [SimpleCov::Formatter::JSONFormatter, SimpleCov::Formatter::HTMLFormatter]
SimpleCov.start do
add_filter 'spec'
add_filter 'lib/forest_admin_agent/auth/oauth2/oidc_config.rb'
@@ -37,6 +40,7 @@
env_secret: '89719c6d8e2e2de2694c2f220fe2dbf02d5289487364daf1e4c6b13733ed0cdb',
is_production: false,
cache_dir: 'tmp/cache/forest_admin',
+ schema_path: File.join('tmp', '.forestadmin-schema.json'),
forest_server_url: 'https://api.development.forestadmin.com',
debug: true
}
diff --git a/packages/forest_admin_datasource_active_record/.gitignore b/packages/forest_admin_datasource_active_record/.gitignore
new file mode 100644
index 000000000..971206634
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/.gitignore
@@ -0,0 +1,15 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+/spec/dummy/tmp/
+/spec/dummy/log/
+/spec/dummy/db/*.db
+
+# rspec failure tracking
+.rspec_status
diff --git a/packages/forest_admin_datasource_active_record/.rspec b/packages/forest_admin_datasource_active_record/.rspec
new file mode 100644
index 000000000..34c5164d9
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/.rspec
@@ -0,0 +1,3 @@
+--format documentation
+--color
+--require spec_helper
diff --git a/packages/forest_admin_datasource_active_record/Gemfile b/packages/forest_admin_datasource_active_record/Gemfile
new file mode 100644
index 000000000..a20f8da71
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/Gemfile
@@ -0,0 +1,20 @@
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in forest_admin_datasource_active_record.gemspec
+gemspec
+
+gem 'forest_admin_datasource_toolkit', path: '../forest_admin_datasource_toolkit'
+
+gem 'rake', '~> 13.0'
+
+gem 'rubocop', '~> 1.21'
+
+group :development, :test do
+ gem 'database_cleaner-active_record'
+ gem 'rails'
+ gem 'rspec-rails', '~> 3.0'
+ gem 'simplecov', '~> 0.22', require: false
+ gem 'simplecov-html', '~> 0.12.3'
+ gem 'simplecov_json_formatter', '~> 0.1.4'
+ gem 'sqlite3'
+end
diff --git a/packages/forest_admin_datasource_active_record/README.md b/packages/forest_admin_datasource_active_record/README.md
new file mode 100644
index 000000000..cbcda87ad
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/README.md
@@ -0,0 +1,31 @@
+# ForestAdminDatasourceActiveRecord
+
+TODO: Delete this and the text below, and describe your gem
+
+Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/forest_admin_datasource_active_record`. To experiment with that code, run `bin/console` for an interactive prompt.
+
+## Installation
+
+TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
+
+Install the gem and add to the application's Gemfile by executing:
+
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
+
+If bundler is not being used to manage dependencies, install the gem by executing:
+
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
+
+## Usage
+
+TODO: Write usage instructions here
+
+## Development
+
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+
+To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/forest_admin_datasource_active_record.
diff --git a/packages/forest_admin_datasource_active_record/Rakefile b/packages/forest_admin_datasource_active_record/Rakefile
new file mode 100644
index 000000000..cca717544
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/Rakefile
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+require "rubocop/rake_task"
+
+RuboCop::RakeTask.new
+
+task default: %i[spec rubocop]
diff --git a/packages/forest_admin_datasource_active_record/bin/console b/packages/forest_admin_datasource_active_record/bin/console
new file mode 100755
index 000000000..5de624b7f
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/bin/console
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "forest_admin_datasource_active_record"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/packages/forest_admin_datasource_active_record/bin/setup b/packages/forest_admin_datasource_active_record/bin/setup
new file mode 100755
index 000000000..dce67d860
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
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
new file mode 100644
index 000000000..96275fcfa
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec
@@ -0,0 +1,37 @@
+lib = File.expand_path('lib', __dir__)
+$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
+
+require_relative "lib/forest_admin_datasource_active_record/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "forest_admin_datasource_active_record"
+ spec.version = ForestAdminDatasourceActiveRecord::VERSION
+ spec.authors = ["Matthieu", "Nicolas"]
+ spec.email = ["matthv@gmail.com", "nicolasalexandre9@gmail.com"]
+ spec.homepage = "https://www.forestadmin.com"
+ spec.summary = "Ruby agent for Forest Admin."
+ spec.description = "Forest is a modern admin interface that works on all major web frameworks. This gem makes Forest
+admin work on any Ruby application."
+ spec.license = "MIT"
+ spec.required_ruby_version = ">= 3.0.0"
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = "https://github.com/ForestAdmin/agent-ruby"
+ spec.metadata["changelog_uri"] = "https://github.com/ForestAdmin/agent-ruby/CHANGELOG.md"
+ spec.metadata["rubygems_mfa_required"] = "true"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(__dir__) do
+ `git ls-files -z`.split("\x0").reject do |f|
+ (File.expand_path(f) == __FILE__) ||
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
+ end
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "activerecord", ">= 6.1"
+ spec.add_dependency "zeitwerk", "~> 2.3"
+end
diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record.rb
new file mode 100644
index 000000000..af99d3703
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record.rb
@@ -0,0 +1,11 @@
+require_relative 'forest_admin_datasource_active_record/version'
+# require 'forest_admin_datasource_toolkit'
+require 'zeitwerk'
+
+loader = Zeitwerk::Loader.for_gem
+loader.setup
+
+module ForestAdminDatasourceActiveRecord
+ class Error < StandardError; end
+ # Your code goes here...
+end
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
new file mode 100644
index 000000000..6ebb6c45e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb
@@ -0,0 +1,83 @@
+module ForestAdminDatasourceActiveRecord
+ class Collection < ForestAdminDatasourceToolkit::Collection
+ include Parser::Column
+ include Parser::Relation
+
+ def initialize(datasource, model)
+ @model = model
+ name = model.name.split('::').last
+ super(datasource, name)
+ fetch_fields
+ fetch_associations
+ end
+
+ private
+
+ def fetch_fields
+ @model.columns_hash.each do |column_name, column|
+ # TODO: check is not sti column
+ field = ForestAdminDatasourceToolkit::Schema::ColumnSchema.new(
+ column_type: get_column_type(@model, column),
+ # filter_operators: [],
+ is_primary_key: column_name == @model.primary_key,
+ is_read_only: false,
+ is_sortable: true,
+ default_value: column.default,
+ enum_values: get_enum_values(@model, column),
+ # validations: get_validations(column)
+ validations: []
+ )
+
+ add_field(column_name, field)
+ end
+ end
+
+ def fetch_associations
+ associations(@model).each do |association|
+ case association.macro
+ when :has_one
+ add_field(
+ association.name.to_s,
+ ForestAdminDatasourceToolkit::Schema::Relations::OneToOneSchema.new(
+ foreign_collection: association.class_name,
+ origin_key: association.foreign_key,
+ origin_key_target: association.join_foreign_key
+ )
+ )
+ when :belongs_to
+ add_field(
+ association.name.to_s,
+ ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema.new(
+ foreign_collection: association.class_name,
+ foreign_key: association.foreign_key,
+ foreign_key_target: association.join_foreign_key
+ )
+ )
+ when :has_many
+ if association.through_reflection?
+ add_field(
+ association.name.to_s,
+ ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new(
+ foreign_collection: association.class_name,
+ origin_key: association.through_reflection.join_foreign_key,
+ origin_key_target: association.through_reflection.foreign_key,
+ foreign_key: association.join_foreign_key,
+ foreign_key_target: association.association_primary_key,
+ through_collection: association.through_reflection.class_name
+ )
+ )
+ else
+ add_field(
+ association.name.to_s,
+ ForestAdminDatasourceToolkit::Schema::Relations::OneToManySchema.new(
+ foreign_collection: association.class_name,
+ origin_key: association.foreign_key,
+ origin_key_target: association.join_foreign_key
+ )
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb
new file mode 100644
index 000000000..f0e39d99a
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb
@@ -0,0 +1,29 @@
+require 'active_record'
+
+module ForestAdminDatasourceActiveRecord
+ class Datasource < ForestAdminDatasourceToolkit::Datasource
+ def initialize(db_config = {})
+ super()
+ @models = []
+ init_orm(db_config)
+ generate
+ end
+
+ private
+
+ def generate
+ ActiveRecord::Base.descendants.each { |model| fetch_model(model) }
+ @models.each do |model|
+ add_collection(Collection.new(self, model))
+ end
+ end
+
+ def fetch_model(model)
+ @models << model unless model.abstract_class? || @models.include?(model) || !model.table_exists?
+ end
+
+ def init_orm(db_config)
+ ActiveRecord::Base.establish_connection(db_config)
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb
new file mode 100644
index 000000000..3ad1272bd
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb
@@ -0,0 +1,49 @@
+module ForestAdminDatasourceActiveRecord
+ module Parser
+ module Column
+ TYPES = {
+ boolean: 'Boolean',
+ datetime: 'Date',
+ date: 'Dateonly',
+ integer: 'Number',
+ float: 'Number',
+ decimal: 'Number',
+ json: 'Json',
+ jsonb: 'Json',
+ hstore: 'Json',
+ string: 'String',
+ text: 'String',
+ citext: 'String',
+ time: 'Time',
+ uuid: 'Uuid',
+ binary: 'Binary'
+ }.freeze
+
+ def get_column_type(model, column)
+ if model.respond_to?(:defined_enums) &&
+ model.defined_enums.key?(column.name)
+ return 'Enum'
+ end
+
+ is_array = (column.respond_to?(:array) && column.array == true)
+ is_array ? "[#{TYPES[column.type]}]" : TYPES[column.type]
+ end
+
+ def get_enum_values(model, column)
+ enum_values = []
+ if get_column_type(model, column) == 'Enum'
+ if sti_column?(model, column)
+ model.descendants.each { |sti_model| enum_values << sti_model.name }
+ else
+ model.defined_enums[column.name].each { |name, _value| enum_values << name }
+ end
+ end
+ enum_values
+ end
+
+ def sti_column?(model, column)
+ model.inheritance_column && column.name == model.inheritance_column
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/relation.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/relation.rb
new file mode 100644
index 000000000..2682a5932
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/relation.rb
@@ -0,0 +1,21 @@
+module ForestAdminDatasourceActiveRecord
+ module Parser
+ module Relation
+ def associations(model)
+ model.reflect_on_all_associations.select do |association|
+ !polymorphic?(association) && !active_type?(association.klass)
+ end
+ end
+
+ def polymorphic?(association)
+ association.options[:polymorphic]
+ end
+
+ # NOTICE: Ignores ActiveType::Object association during introspection and interactions.
+ # See the gem documentation: https://github.com/makandra/active_type
+ def active_type?(model)
+ Object.const_defined?('ActiveType::Object') && model < ActiveType::Object
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb
new file mode 100644
index 000000000..cb65efa24
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb
@@ -0,0 +1,108 @@
+module ForestAdminDatasourceActiveRecord
+ module Parser
+ module Validation
+ def get_validations(column)
+ validations = []
+ # NOTICE: Do not consider validations if a before_validation Active Records
+ # Callback is detected.
+ return validations if @model._validation_callbacks.map(&:kind).include? :before
+
+ if @model._validators? && @model._validators[column.name.to_sym].size.positive?
+ @model._validators[column.name.to_sym].each do |validator|
+ # NOTICE: Do not consider conditional validations
+ next if validator.options[:if] || validator.options[:unless] || validator.options[:on]
+
+ case validator
+ when ActiveRecord::Validations::PresenceValidator
+ validations << {
+ type: 'is present',
+ message: validator.options[:message]
+ }
+ when ActiveModel::Validations::NumericalityValidator
+ validations = parse_numericality_validator(validator, validations)
+ when ActiveModel::Validations::LengthValidator
+ validations = parse_length_validator(validator, validations)
+ when ActiveModel::Validations::FormatValidator
+ validations = parse_format_validator(validator, validations)
+ end
+ end
+ end
+
+ validations
+ end
+
+ def parse_numericality_validator(validator, parsed_validations)
+ validator.options.each do |option, value|
+ case option
+ when :greater_than, :greater_than_or_equal_to
+ parsed_validations << {
+ type: 'is greater than',
+ value: value,
+ message: validator.options[:message]
+ }
+ when :less_than, :less_than_or_equal_to
+ parsed_validations << {
+ type: 'is less than',
+ value: value,
+ message: validator.options[:message]
+ }
+ end
+ end
+ end
+
+ def parse_length_validator(validator, parsed_validations)
+ return unless get_column_type(column) == 'String'
+
+ validator.options.each do |option, value|
+ case option
+ when :minimum
+ parsed_validations << {
+ type: 'is longer than',
+ value: value,
+ message: validator.options[:message]
+ }
+ when :maximum
+ parsed_validations << {
+ type: 'is shorter than',
+ value: value,
+ message: validator.options[:message]
+ }
+ when :is
+ parsed_validations << {
+ type: 'is longer than',
+ value: value,
+ message: validator.options[:message]
+ }
+ parsed_validations << {
+ type: 'is shorter than',
+ value: value,
+ message: validator.options[:message]
+ }
+ end
+ end
+ end
+
+ def parse_format_validator(validator, parsed_validations)
+ validator.options.each do |option, value|
+ case option
+ when :with
+ options = /\?([imx]){0,3}/.match(validator.options[:with].to_s)
+ options = options && options[1] ? options[1] : ''
+ regex = value.source
+
+ # NOTICE: Transform a Ruby regex into a JS one
+ regex = regex.sub('\\A', '^').sub('\\Z', '$').sub('\\z', '$').gsub(/\n+|\s+/, '')
+
+ parsed_validations << {
+ type: 'is like',
+ value: "/#{regex}/#{options}",
+ message: validator.options[:message]
+ }
+ end
+ end
+
+ parsed_validations
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb
new file mode 100644
index 000000000..66a0a3f88
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb
@@ -0,0 +1,3 @@
+module ForestAdminDatasourceActiveRecord
+ VERSION = "0.1.0"
+end
diff --git a/packages/forest_admin_datasource_active_record/sig/forest_admin_datasource_active_record.rbs b/packages/forest_admin_datasource_active_record/sig/forest_admin_datasource_active_record.rbs
new file mode 100644
index 000000000..e43641298
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/sig/forest_admin_datasource_active_record.rbs
@@ -0,0 +1,4 @@
+module ForestAdminDatasourceActiveRecord
+ VERSION: String
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
+end
diff --git a/packages/forest_admin_datasource_active_record/sig/forest_admin_datasource_active_record/collection.rbs b/packages/forest_admin_datasource_active_record/sig/forest_admin_datasource_active_record/collection.rbs
new file mode 100644
index 000000000..54aff43fe
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/sig/forest_admin_datasource_active_record/collection.rbs
@@ -0,0 +1,11 @@
+module ForestAdminDatasourceActiveRecord
+ class Collection
+ @model: untyped
+
+ private
+
+ def fetch_fields: -> void
+
+ def get_enum_values: -> [String]
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/Rakefile b/packages/forest_admin_datasource_active_record/spec/dummy/Rakefile
new file mode 100644
index 000000000..9a5ea7383
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/Rakefile
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative "config/application"
+
+Rails.application.load_tasks
diff --git a/packages/forest_admin_rails/app/models/concerns/.keep b/packages/forest_admin_datasource_active_record/spec/dummy/app/assets/images/.keep
similarity index 100%
rename from packages/forest_admin_rails/app/models/concerns/.keep
rename to packages/forest_admin_datasource_active_record/spec/dummy/app/assets/images/.keep
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/assets/stylesheets/application.css b/packages/forest_admin_datasource_active_record/spec/dummy/app/assets/stylesheets/application.css
new file mode 100644
index 000000000..dcd72732e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/assets/stylesheets/application.css
@@ -0,0 +1 @@
+/* Application styles */
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/channels/application_cable/channel.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/channels/application_cable/channel.rb
new file mode 100644
index 000000000..d67269728
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/channels/application_cable/channel.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/channels/application_cable/connection.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/channels/application_cable/connection.rb
new file mode 100644
index 000000000..0ff5442f4
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/channels/application_cable/connection.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/controllers/application_controller.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/controllers/application_controller.rb
new file mode 100644
index 000000000..09705d12a
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/packages/forest_admin_datasource_active_record/.gitkeep b/packages/forest_admin_datasource_active_record/spec/dummy/app/controllers/concerns/.keep
similarity index 100%
rename from packages/forest_admin_datasource_active_record/.gitkeep
rename to packages/forest_admin_datasource_active_record/spec/dummy/app/controllers/concerns/.keep
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/helpers/application_helper.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/helpers/application_helper.rb
new file mode 100644
index 000000000..de6be7945
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/jobs/application_job.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/jobs/application_job.rb
new file mode 100644
index 000000000..d394c3d10
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/jobs/application_job.rb
@@ -0,0 +1,7 @@
+class ApplicationJob < ActiveJob::Base
+ # Automatically retry jobs that encountered a deadlock
+ # retry_on ActiveRecord::Deadlocked
+
+ # Most jobs are safe to ignore if the underlying records are no longer available
+ # discard_on ActiveJob::DeserializationError
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/mailers/application_mailer.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/mailers/application_mailer.rb
new file mode 100644
index 000000000..3c34c8148
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: "from@example.com"
+ layout "mailer"
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/address.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/address.rb
new file mode 100644
index 000000000..d80b23795
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/address.rb
@@ -0,0 +1,3 @@
+class Address < ApplicationRecord
+ belongs_to :addressable, polymorphic: true
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/application_record.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/application_record.rb
new file mode 100644
index 000000000..b63caeb8a
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ primary_abstract_class
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/car.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/car.rb
new file mode 100644
index 000000000..0b49880b0
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/car.rb
@@ -0,0 +1,6 @@
+class Car < ApplicationRecord
+ belongs_to :category
+ has_one :user
+ has_many :car_checks
+ has_many :checks, through: :car_checks
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/car_check.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/car_check.rb
new file mode 100644
index 000000000..d97b0158a
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/car_check.rb
@@ -0,0 +1,4 @@
+class CarCheck < ApplicationRecord
+ belongs_to :car
+ belongs_to :check
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/category.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/category.rb
new file mode 100644
index 000000000..54cb6aee3
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/category.rb
@@ -0,0 +1,2 @@
+class Category < ApplicationRecord
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/check.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/check.rb
new file mode 100644
index 000000000..afc1b25cb
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/check.rb
@@ -0,0 +1,4 @@
+class Check < ApplicationRecord
+ has_many :car_checks
+ has_many :cars, through: :car_checks
+end
diff --git a/packages/forest_admin_rails/app/models/forest_admin_rails/application_record.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/concerns/.keep
similarity index 100%
rename from packages/forest_admin_rails/app/models/forest_admin_rails/application_record.rb
rename to packages/forest_admin_datasource_active_record/spec/dummy/app/models/concerns/.keep
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/order.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/order.rb
new file mode 100644
index 000000000..10281b345
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/order.rb
@@ -0,0 +1,2 @@
+class Order < ApplicationRecord
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/user.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/user.rb
new file mode 100644
index 000000000..f1df24e7e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/user.rb
@@ -0,0 +1,6 @@
+class User < ApplicationRecord
+ belongs_to :car
+ has_one :address, as: :addressable
+
+ enum :enum_field, { draft: 0, published: 1, archived: 2, trashed: 3 }
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/application.html.erb b/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/application.html.erb
new file mode 100644
index 000000000..f72b4ef0e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/application.html.erb
@@ -0,0 +1,15 @@
+
+
+
+ Dummy
+
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= stylesheet_link_tag "application" %>
+
+
+
+ <%= yield %>
+
+
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/mailer.html.erb b/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/mailer.html.erb
new file mode 100644
index 000000000..cbd34d2e9
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/mailer.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+ <%= yield %>
+
+
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/mailer.text.erb b/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/mailer.text.erb
new file mode 100644
index 000000000..37f0bddbd
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/bin/rails b/packages/forest_admin_datasource_active_record/spec/dummy/bin/rails
new file mode 100755
index 000000000..efc037749
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/bin/rake b/packages/forest_admin_datasource_active_record/spec/dummy/bin/rake
new file mode 100755
index 000000000..4fbf10b96
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative "../config/boot"
+require "rake"
+Rake.application.run
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/bin/setup b/packages/forest_admin_datasource_active_record/spec/dummy/bin/setup
new file mode 100755
index 000000000..ec47b79b3
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/bin/setup
@@ -0,0 +1,33 @@
+#!/usr/bin/env ruby
+require "fileutils"
+
+# path to your application root.
+APP_ROOT = File.expand_path("..", __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+FileUtils.chdir APP_ROOT do
+ # This script is a way to set up or update your development environment automatically.
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
+ # Add necessary setup steps to this file.
+
+ puts "== Installing dependencies =="
+ system! "gem install bundler --conservative"
+ system("bundle check") || system!("bundle install")
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # FileUtils.cp "config/database.yml.sample", "config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system! "bin/rails db:prepare"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! "bin/rails log:clear tmp:clear"
+
+ puts "\n== Restarting application server =="
+ system! "bin/rails restart"
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config.ru b/packages/forest_admin_datasource_active_record/spec/dummy/config.ru
new file mode 100644
index 000000000..4a3c09a68
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config.ru
@@ -0,0 +1,6 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative "config/environment"
+
+run Rails.application
+Rails.application.load_server
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/application.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/application.rb
new file mode 100644
index 000000000..fa0d753db
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/application.rb
@@ -0,0 +1,21 @@
+require_relative "boot"
+
+require "rails/all"
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Dummy
+ class Application < Rails::Application
+ config.load_defaults Rails::VERSION::STRING.to_f
+
+ # Configuration for the application, engines, and railties goes here.
+ #
+ # These settings can be overridden in specific environments using the files
+ # in config/environments, which are processed later.
+ #
+ # config.time_zone = "Central Time (US & Canada)"
+ # config.eager_load_paths << Rails.root.join("extras")
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/boot.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/boot.rb
new file mode 100644
index 000000000..116591a4e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/boot.rb
@@ -0,0 +1,5 @@
+# Set up gems listed in the Gemfile.
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
+
+require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
+$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/cable.yml b/packages/forest_admin_datasource_active_record/spec/dummy/config/cable.yml
new file mode 100644
index 000000000..98367f895
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/cable.yml
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: test
+
+production:
+ adapter: redis
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: dummy_production
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/database.yml b/packages/forest_admin_datasource_active_record/spec/dummy/config/database.yml
new file mode 100644
index 000000000..f3753ab95
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/database.yml
@@ -0,0 +1,12 @@
+default: &default
+ adapter: sqlite3
+ database: 'db/database.db'
+
+development:
+ <<: *default
+
+test:
+ <<: *default
+
+production:
+ <<: *default
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/environment.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/environment.rb
new file mode 100644
index 000000000..cac531577
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative "application"
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/development.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/development.rb
new file mode 100644
index 000000000..8d1635ee6
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/development.rb
@@ -0,0 +1,68 @@
+require "active_support/core_ext/integer/time"
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded any time
+ # it changes. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable server timing
+ config.server_timing = true
+
+ # Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join("tmp/caching-dev.txt").exist?
+ config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+
+ # Uncomment if you wish to allow Action Cable access from any origin.
+ # config.action_cable.disable_request_forgery_protection = true
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/production.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/production.rb
new file mode 100644
index 000000000..83a5a16ee
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/production.rb
@@ -0,0 +1,87 @@
+require "active_support/core_ext/integer/time"
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.asset_host = "http://assets.example.com"
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
+ # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Mount Action Cable outside main process or domain.
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = "wss://example.com/cable"
+ # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Include generic and useful information about system operation, but avoid logging too much
+ # information to avoid inadvertent exposure of personally identifiable information (PII).
+ config.log_level = :info
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment).
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "dummy_production"
+
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Don't log any deprecations.
+ config.active_support.report_deprecations = false
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require "syslog/logger"
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/test.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/test.rb
new file mode 100644
index 000000000..6ea4d1e70
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/environments/test.rb
@@ -0,0 +1,60 @@
+require "active_support/core_ext/integer/time"
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Turn false under Spring and add config.action_view.cache_template_loading = true.
+ config.cache_classes = true
+
+ # Eager loading loads your whole application. When running a single test locally,
+ # this probably isn't necessary. It's a good idea to do in a continuous integration
+ # system, or in some way before deploying your code.
+ config.eager_load = ENV["CI"].present?
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{1.hour.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+ config.cache_store = :null_store
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Store uploaded files on the local file system in a temporary directory.
+ config.active_storage.service = :test
+
+ config.action_mailer.perform_caching = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/content_security_policy.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/content_security_policy.rb
new file mode 100644
index 000000000..54f47cf15
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/content_security_policy.rb
@@ -0,0 +1,25 @@
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy.
+# See the Securing Rails Applications Guide for more information:
+# https://guides.rubyonrails.org/security.html#content-security-policy-header
+
+# Rails.application.configure do
+# config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+#
+# # Generate session nonces for permitted importmap and inline scripts
+# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
+# config.content_security_policy_nonce_directives = %w(script-src)
+#
+# # Report violations without enforcing the policy.
+# # config.content_security_policy_report_only = true
+# end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/filter_parameter_logging.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 000000000..adc6568ce
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure parameters to be filtered from the log file. Use this to limit dissemination of
+# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
+# notations and behaviors.
+Rails.application.config.filter_parameters += [
+ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
+]
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/inflections.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/inflections.rb
new file mode 100644
index 000000000..3860f659e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, "\\1en"
+# inflect.singular /^(ox)en/i, "\\1"
+# inflect.irregular "person", "people"
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym "RESTful"
+# end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/permissions_policy.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/permissions_policy.rb
new file mode 100644
index 000000000..00f64d71b
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/initializers/permissions_policy.rb
@@ -0,0 +1,11 @@
+# Define an application-wide HTTP permissions policy. For further
+# information see https://developers.google.com/web/updates/2018/06/feature-policy
+#
+# Rails.application.config.permissions_policy do |f|
+# f.camera :none
+# f.gyroscope :none
+# f.microphone :none
+# f.usb :none
+# f.fullscreen :self
+# f.payment :self, "https://secure.example.com"
+# end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/locales/en.yml b/packages/forest_admin_datasource_active_record/spec/dummy/config/locales/en.yml
new file mode 100644
index 000000000..8ca56fc74
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t "hello"
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t("hello") %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+# "true": "foo"
+#
+# To learn more, please read the Rails Internationalization guide
+# available at https://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/puma.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/puma.rb
new file mode 100644
index 000000000..daaf03699
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/puma.rb
@@ -0,0 +1,43 @@
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+#
+max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
+threads min_threads_count, max_threads_count
+
+# Specifies the `worker_timeout` threshold that Puma will use to wait before
+# terminating a worker in development environments.
+#
+worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch("PORT") { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch("RAILS_ENV") { "development" }
+
+# Specifies the `pidfile` that Puma will use.
+pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked web server processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory.
+#
+# preload_app!
+
+# Allow puma to be restarted by `bin/rails restart` command.
+plugin :tmp_restart
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/routes.rb b/packages/forest_admin_datasource_active_record/spec/dummy/config/routes.rb
new file mode 100644
index 000000000..262ffd547
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/routes.rb
@@ -0,0 +1,6 @@
+Rails.application.routes.draw do
+ # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
+
+ # Defines the root path route ("/")
+ # root "articles#index"
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/config/storage.yml b/packages/forest_admin_datasource_active_record/spec/dummy/config/storage.yml
new file mode 100644
index 000000000..4942ab669
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/config/storage.yml
@@ -0,0 +1,34 @@
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# project: your_project
+# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
+# microsoft:
+# service: AzureStorage
+# storage_account_name: your_account_name
+# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
+# container: your_container_name-<%= Rails.env %>
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810094706_create_addresses.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810094706_create_addresses.rb
new file mode 100644
index 000000000..72e8a6a39
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810094706_create_addresses.rb
@@ -0,0 +1,11 @@
+class CreateAddresses < ActiveRecord::Migration[7.0]
+ def change
+ create_table :addresses do |t|
+ t.string :city
+ t.string :zip_code
+ t.string :street
+ t.references :addressable, polymorphic: true
+ t.timestamps
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810094707_create_categories.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810094707_create_categories.rb
new file mode 100644
index 000000000..5d9d1db53
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810094707_create_categories.rb
@@ -0,0 +1,9 @@
+class CreateCategories < ActiveRecord::Migration[7.0]
+ def change
+ create_table :categories do |t|
+ t.string :label
+
+ t.timestamps
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810095724_create_cars.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810095724_create_cars.rb
new file mode 100644
index 000000000..cb65d175a
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810095724_create_cars.rb
@@ -0,0 +1,16 @@
+class CreateCars < ActiveRecord::Migration[7.0]
+ def change
+ create_table :cars do |t|
+ t.references :category, null: false, foreign_key: true
+ t.string :reference
+ t.string :model
+ t.string :brand
+ t.integer :year
+ t.integer :nb_seats
+ t.boolean :is_manual
+ t.json :options
+
+ t.timestamps
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810095725_create_users.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810095725_create_users.rb
new file mode 100644
index 000000000..2911bf78e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810095725_create_users.rb
@@ -0,0 +1,29 @@
+class CreateUsers < ActiveRecord::Migration[7.0]
+ def change
+ create_table :users do |t|
+ t.references :car, null: false, foreign_key: true
+
+ t.string :first_name
+ t.string :last_name
+
+ t.string :string_field
+ t.text :text_field
+ t.boolean :boolean_field
+ t.date :date_field
+ t.datetime :datetime_field
+ t.integer :integer_field
+ t.float :float_field
+ t.decimal :decimal_field
+ t.json :json_field
+ t.time :time_field
+ t.binary :binary_field
+ t.integer :enum_field, default: 0
+ # t.jsonb :options_b
+ # t.hstore :options_hstore
+ # t.citext :citext_field
+ # t.uuid :uuid
+
+ t.timestamps
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810102316_create_checks.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810102316_create_checks.rb
new file mode 100644
index 000000000..90874d97e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810102316_create_checks.rb
@@ -0,0 +1,10 @@
+class CreateChecks < ActiveRecord::Migration[7.0]
+ def change
+ create_table :checks do |t|
+ t.string :garage_name
+ t.date :date
+
+ t.timestamps
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810123423_create_join_table_car_check.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810123423_create_join_table_car_check.rb
new file mode 100644
index 000000000..1b69d5814
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230810123423_create_join_table_car_check.rb
@@ -0,0 +1,8 @@
+class CreateJoinTableCarCheck < ActiveRecord::Migration[7.0]
+ def change
+ create_table :car_checks do |t|
+ t.references :car, null: false, foreign_key: true
+ t.references :check, null: false, foreign_key: true
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230816094641_create_orders.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230816094641_create_orders.rb
new file mode 100644
index 000000000..d0e650766
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20230816094641_create_orders.rb
@@ -0,0 +1,10 @@
+class CreateOrders < ActiveRecord::Migration[7.0]
+ def change
+ create_table :orders do |t|
+ t.string :reference
+ t.binary :invoice
+
+ t.timestamps
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/lib/assets/.keep b/packages/forest_admin_datasource_active_record/spec/dummy/lib/assets/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/log/.keep b/packages/forest_admin_datasource_active_record/spec/dummy/log/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/public/404.html b/packages/forest_admin_datasource_active_record/spec/dummy/public/404.html
new file mode 100644
index 000000000..2be3af26f
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/public/404.html
@@ -0,0 +1,67 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/public/422.html b/packages/forest_admin_datasource_active_record/spec/dummy/public/422.html
new file mode 100644
index 000000000..c08eac0d1
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/public/500.html b/packages/forest_admin_datasource_active_record/spec/dummy/public/500.html
new file mode 100644
index 000000000..78a030af2
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/public/apple-touch-icon-precomposed.png b/packages/forest_admin_datasource_active_record/spec/dummy/public/apple-touch-icon-precomposed.png
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/public/apple-touch-icon.png b/packages/forest_admin_datasource_active_record/spec/dummy/public/apple-touch-icon.png
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/public/favicon.ico b/packages/forest_admin_datasource_active_record/spec/dummy/public/favicon.ico
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/storage/.keep b/packages/forest_admin_datasource_active_record/spec/dummy/storage/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/tmp/.keep b/packages/forest_admin_datasource_active_record/spec/dummy/tmp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/tmp/development_secret.txt b/packages/forest_admin_datasource_active_record/spec/dummy/tmp/development_secret.txt
new file mode 100644
index 000000000..2f91f53a8
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/dummy/tmp/development_secret.txt
@@ -0,0 +1 @@
+5e7e529d64029e290d92ee62bd2967000b9959e0fa32eb8168810376b9193585f73a12c712c87de927e927d8b6487730b15a9d97c6aab0402885bc726bd4bf30
\ No newline at end of file
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/tmp/pids/.keep b/packages/forest_admin_datasource_active_record/spec/dummy/tmp/pids/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/tmp/storage/.keep b/packages/forest_admin_datasource_active_record/spec/dummy/tmp/storage/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/forest_admin_datasource_active_record/spec/forest_admin_datasource_active_record_spec.rb b/packages/forest_admin_datasource_active_record/spec/forest_admin_datasource_active_record_spec.rb
new file mode 100644
index 000000000..ca619246e
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/forest_admin_datasource_active_record_spec.rb
@@ -0,0 +1,5 @@
+RSpec.describe ForestAdminDatasourceActiveRecord do
+ it 'has a version number' do
+ expect(ForestAdminDatasourceActiveRecord::VERSION).not_to be_nil
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb
new file mode 100644
index 000000000..dad32a50c
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceActiveRecord
+ describe Collection do
+ let(:collection) do
+ datasource = Datasource.new(adapter: 'sqlite3', database: 'db/database.db')
+ described_class.new(datasource, Car)
+ end
+
+ describe 'fetch_fields' do
+ it 'add all fields of model to the collection' do
+ expect(collection.fields.keys).to include(
+ 'id',
+ 'category_id',
+ 'reference',
+ 'model',
+ 'brand',
+ 'year',
+ 'nb_seats',
+ 'is_manual',
+ 'options',
+ 'created_at',
+ 'updated_at'
+ )
+ end
+ end
+
+ describe 'fetch_associations' do
+ it 'add all relation of model to the collection' do
+ expect(collection.fields.keys).to include('category', 'user', 'car_checks', 'checks')
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb
new file mode 100644
index 000000000..c7e6a468d
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceActiveRecord
+ describe Datasource do
+ it 'fetch all models' do
+ datasource = described_class.new(adapter: 'sqlite3', database: 'db/database.db')
+ expected = %w[InternalMetadata SchemaMigration User Order Check Category CarCheck Car Address]
+
+ expect(datasource.collections.keys).to match_array(expected)
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/column_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/column_spec.rb
new file mode 100644
index 000000000..522771c6c
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/column_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceActiveRecord
+ module Parser
+ describe Column do
+ let(:dummy_class) { Class.new { extend Column } }
+ let(:columns) { User.columns_hash }
+
+ describe 'get_column_type' do
+ it { expect(dummy_class.get_column_type(User, columns['string_field'])).to eq 'String' }
+ it { expect(dummy_class.get_column_type(User, columns['text_field'])).to eq 'String' }
+ it { expect(dummy_class.get_column_type(User, columns['boolean_field'])).to eq 'Boolean' }
+ it { expect(dummy_class.get_column_type(User, columns['date_field'])).to eq 'Dateonly' }
+ it { expect(dummy_class.get_column_type(User, columns['datetime_field'])).to eq 'Date' }
+ it { expect(dummy_class.get_column_type(User, columns['integer_field'])).to eq 'Number' }
+ it { expect(dummy_class.get_column_type(User, columns['float_field'])).to eq 'Number' }
+ it { expect(dummy_class.get_column_type(User, columns['decimal_field'])).to eq 'Number' }
+ it { expect(dummy_class.get_column_type(User, columns['json_field'])).to eq 'Json' }
+ it { expect(dummy_class.get_column_type(User, columns['time_field'])).to eq 'Time' }
+ it { expect(dummy_class.get_column_type(User, columns['binary_field'])).to eq 'Binary' }
+ it { expect(dummy_class.get_column_type(User, columns['enum_field'])).to eq 'Enum' }
+ end
+
+ describe 'get_enum_values' do
+ it 'return an empty array if column is not Enum' do
+ expect(dummy_class.get_enum_values(User, columns['string_field'])).to eq []
+ end
+
+ it 'return the enum values' do
+ expect(dummy_class.get_enum_values(User, columns['enum_field'])).to eq(%w[draft published archived trashed])
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/relation_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/relation_spec.rb
new file mode 100644
index 000000000..73d429d49
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/relation_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceActiveRecord
+ module Parser
+ describe Relation do
+ let(:dummy_class) { Class.new { extend Relation } }
+
+ describe 'associations' do
+ it 'fetch belongs_to relation' do
+ associations = dummy_class.associations(Car)
+ association = associations.select { |a| a.macro == :belongs_to }
+
+ expect(association).not_to be_nil
+ end
+
+ it 'fetch has_one relation' do
+ associations = dummy_class.associations(Car)
+ association = associations.select { |a| a.macro == :has_one }
+
+ expect(association).not_to be_nil
+ end
+
+ it 'fetch has_many relation' do
+ associations = dummy_class.associations(Car)
+ association = associations.select { |a| a.macro == :has_many }
+
+ expect(association).not_to be_nil
+ end
+
+ it 'fetch many_to_many relation defined with has_many through' do
+ associations = dummy_class.associations(Car)
+ association = associations.select { |a| a.macro == :has_many && a.through_reflection? }
+
+ expect(association).not_to be_nil
+ end
+
+ it 'not fetch polymorphic relation' do
+ associations = dummy_class.associations(Car)
+ association = associations.select { |a| a.options[:polymorphic] }
+
+ expect(association).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_active_record/spec/spec_helper.rb b/packages/forest_admin_datasource_active_record/spec/spec_helper.rb
new file mode 100644
index 000000000..02fd111e4
--- /dev/null
+++ b/packages/forest_admin_datasource_active_record/spec/spec_helper.rb
@@ -0,0 +1,44 @@
+require 'simplecov'
+require 'simplecov_json_formatter'
+require 'simplecov-html'
+require 'forest_admin_datasource_active_record'
+require 'active_record'
+require 'database_cleaner-active_record'
+
+SimpleCov.formatters = [SimpleCov::Formatter::JSONFormatter, SimpleCov::Formatter::HTMLFormatter]
+SimpleCov.start do
+ add_filter 'spec'
+end
+
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('dummy/config/environment', __dir__)
+Rails.application.eager_load!
+
+ActiveRecord::Migration.verbose = false
+ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'db/database.db')
+ActiveRecord::MigrationContext.new("spec/dummy/db/migrate").migrate
+
+RSpec.configure do |config|
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+
+ # database_cleaner config
+ config.before(:suite) do
+ DatabaseCleaner.clean_with :truncation, except: %w[ar_internal_metadata]
+ end
+
+ config.before do
+ DatabaseCleaner.strategy = :transaction
+ end
+
+ config.before do
+ DatabaseCleaner.start
+ end
+
+ config.after do
+ DatabaseCleaner.clean
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/.gitignore b/packages/forest_admin_datasource_toolkit/.gitignore
new file mode 100644
index 000000000..b04a8c840
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/.gitignore
@@ -0,0 +1,11 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
diff --git a/packages/forest_admin_datasource_toolkit/.rspec b/packages/forest_admin_datasource_toolkit/.rspec
new file mode 100644
index 000000000..34c5164d9
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/.rspec
@@ -0,0 +1,3 @@
+--format documentation
+--color
+--require spec_helper
diff --git a/packages/forest_admin_datasource_toolkit/Gemfile b/packages/forest_admin_datasource_toolkit/Gemfile
new file mode 100644
index 000000000..05fdf2813
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/Gemfile
@@ -0,0 +1,14 @@
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in forest_admin_datasource_toolkit.gemspec
+gemspec
+
+gem 'rake', '~> 13.0'
+gem 'rubocop', '~> 1.21'
+
+group :development, :test do
+ gem 'rspec', '~> 3.0'
+ gem 'simplecov', '~> 0.22', require: false
+ gem 'simplecov-html', '~> 0.12.3'
+ gem 'simplecov_json_formatter', '~> 0.1.4'
+end
diff --git a/packages/forest_admin_datasource_toolkit/README.md b/packages/forest_admin_datasource_toolkit/README.md
new file mode 100644
index 000000000..6fe50bcbe
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/README.md
@@ -0,0 +1,31 @@
+# ForestAdminDatasourceToolkit
+
+TODO: Delete this and the text below, and describe your gem
+
+Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/forest_admin_datasource_toolkit`. To experiment with that code, run `bin/console` for an interactive prompt.
+
+## Installation
+
+TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
+
+Install the gem and add to the application's Gemfile by executing:
+
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
+
+If bundler is not being used to manage dependencies, install the gem by executing:
+
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
+
+## Usage
+
+TODO: Write usage instructions here
+
+## Development
+
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+
+To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/forest_admin_datasource_toolkit.
diff --git a/packages/forest_admin_datasource_toolkit/Rakefile b/packages/forest_admin_datasource_toolkit/Rakefile
new file mode 100644
index 000000000..cca717544
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/Rakefile
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+require "rubocop/rake_task"
+
+RuboCop::RakeTask.new
+
+task default: %i[spec rubocop]
diff --git a/packages/forest_admin_datasource_toolkit/bin/console b/packages/forest_admin_datasource_toolkit/bin/console
new file mode 100755
index 000000000..105e360de
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/bin/console
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "forest_admin_datasource_toolkit"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/packages/forest_admin_datasource_toolkit/bin/setup b/packages/forest_admin_datasource_toolkit/bin/setup
new file mode 100755
index 000000000..dce67d860
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/packages/forest_admin_datasource_toolkit/forest_admin_datasource_toolkit.gemspec b/packages/forest_admin_datasource_toolkit/forest_admin_datasource_toolkit.gemspec
new file mode 100644
index 000000000..b78b2dc16
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/forest_admin_datasource_toolkit.gemspec
@@ -0,0 +1,36 @@
+lib = File.expand_path('lib', __dir__)
+$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
+
+require_relative "lib/forest_admin_datasource_toolkit/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "forest_admin_datasource_toolkit"
+ spec.version = ForestAdminDatasourceToolkit::VERSION
+ spec.authors = ["Matthieu", "Nicolas"]
+ spec.email = ["matthv@gmail.com", "nicolasalexandre9@gmail.com"]
+ spec.homepage = "https://www.forestadmin.com"
+ spec.summary = "Ruby agent for Forest Admin."
+ spec.description = "Forest is a modern admin interface that works on all major web frameworks. This gem makes Forest
+admin work on any Ruby application."
+ spec.license = "MIT"
+ spec.required_ruby_version = ">= 3.0.0"
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = "https://github.com/ForestAdmin/agent-ruby"
+ spec.metadata["changelog_uri"] = "https://github.com/ForestAdmin/agent-ruby/CHANGELOG.md"
+ spec.metadata["rubygems_mfa_required"] = "true"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(__dir__) do
+ `git ls-files -z`.split("\x0").reject do |f|
+ (File.expand_path(f) == __FILE__) ||
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
+ end
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "zeitwerk", "~> 2.3"
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit.rb
new file mode 100644
index 000000000..57c965153
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit.rb
@@ -0,0 +1,10 @@
+require_relative "forest_admin_datasource_toolkit/version"
+require 'zeitwerk'
+
+loader = Zeitwerk::Loader.for_gem
+loader.setup
+
+module ForestAdminDatasourceToolkit
+ class Error < StandardError; end
+ # Your code goes here...
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb
new file mode 100644
index 000000000..fe8dbf614
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb
@@ -0,0 +1,50 @@
+module ForestAdminDatasourceToolkit
+ class Collection < Components::Contracts::CollectionContract
+ attr_accessor :fields, :segments
+
+ attr_reader :actions,
+ :charts,
+ :datasource,
+ :name,
+ :schema,
+ :native_driver
+
+ attr_writer :searchable,
+ :countable
+
+ def initialize(datasource, name, native_driver: nil)
+ super()
+ @datasource = datasource
+ @name = name
+ @native_driver = native_driver
+ @fields = {}
+ @schema = {}
+ @fields = {}
+ @actions = {}
+ @segments = {}
+ @charts = {}
+ @searchable = false
+ @countable = false
+ end
+
+ def is_countable?
+ @countable
+ end
+
+ def is_searchable?
+ @searchable
+ end
+
+ def add_field(name, field)
+ raise Exceptions::ForestException, "Field #{name} already defined in collection" if @fields.key?(name)
+
+ @fields[name] = field
+ end
+
+ def add_fields(fields)
+ fields.each do |name, field|
+ add_field(name, field)
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb
new file mode 100644
index 000000000..b69c01e7c
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb
@@ -0,0 +1,51 @@
+module ForestAdminDatasourceToolkit
+ module Components
+ module Contracts
+ class CollectionContract
+ def datasource
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def schema
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def name
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def execute
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def form
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def create
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def list
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def update
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def delete
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def aggregate
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def render_chart
+ 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/contracts/datasource_contract.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rb
new file mode 100644
index 000000000..91e0e66b4
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rb
@@ -0,0 +1,27 @@
+module ForestAdminDatasourceToolkit
+ module Components
+ module Contracts
+ class DatasourceContract
+ def collections
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def charts
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def collection(name)
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def add_collection(collection)
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
+ end
+
+ def render_chart(caller, name)
+ 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/datasource.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/datasource.rb
new file mode 100644
index 000000000..a357209e7
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/datasource.rb
@@ -0,0 +1,29 @@
+module ForestAdminDatasourceToolkit
+ class Datasource < Components::Contracts::DatasourceContract
+ attr_reader :collections, :charts
+
+ def initialize
+ super
+ @charts = {}
+ @collections = {}
+ end
+
+ def collection(name)
+ raise Exceptions::ForestException, "Collection #{name} not found." unless @collections.key? name
+
+ @collections[name]
+ end
+
+ def add_collection(collection)
+ if @collections.key? collection.name
+ raise Exceptions::ForestException, "Collection #{collection.name} already defined in datasource"
+ end
+
+ @collections[collection.name] = collection
+ end
+
+ def render_chart(_caller, name)
+ raise Exceptions::ForestException, "No chart named #{name} exists on this datasource."
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb
new file mode 100644
index 000000000..861b2039e
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb
@@ -0,0 +1,10 @@
+module ForestAdminDatasourceToolkit
+ module Exceptions
+ class ForestException < RuntimeError
+ def initialize(msg = '')
+ msg = "🌳🌳🌳 #{msg}"
+ super msg
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/column_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/column_schema.rb
new file mode 100644
index 000000000..d144f7c0f
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/column_schema.rb
@@ -0,0 +1,34 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ class ColumnSchema
+ attr_reader :is_primary_key, :default_value, :enum_values, :type
+
+ attr_accessor :is_read_only,
+ :is_sortable,
+ :validations,
+ :filter_operators,
+ :column_type
+
+ def initialize(
+ column_type:,
+ filter_operators: [],
+ is_primary_key: false,
+ is_read_only: false,
+ is_sortable: false,
+ default_value: nil,
+ enum_values: [],
+ validations: []
+ )
+ @column_type = column_type
+ @filter_operators = filter_operators
+ @is_primary_key = is_primary_key
+ @is_read_only = is_read_only
+ @is_sortable = is_sortable
+ @default_value = default_value
+ @enum_values = enum_values
+ @validations = validations
+ @type = 'Column'
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relation_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relation_schema.rb
new file mode 100644
index 000000000..18377ad74
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relation_schema.rb
@@ -0,0 +1,13 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ class RelationSchema
+ attr_accessor :foreign_collection
+ attr_reader :type
+
+ def initialize(foreign_collection, type)
+ @foreign_collection = foreign_collection
+ @type = type
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb
new file mode 100644
index 000000000..839c4f4d6
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb
@@ -0,0 +1,26 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ class ManyToManySchema < RelationSchema
+ attr_accessor :origin_key, :through_collection, :foreign_key
+ attr_reader :origin_key_target, :foreign_key_target
+
+ def initialize(
+ origin_key:,
+ origin_key_target:,
+ foreign_key:,
+ foreign_key_target:,
+ foreign_collection:,
+ through_collection:
+ )
+ super(foreign_collection, 'ManyToMany')
+ @origin_key = origin_key
+ @origin_key_target = origin_key_target
+ @through_collection = through_collection
+ @foreign_key = foreign_key
+ @foreign_key_target = foreign_key_target
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema.rb
new file mode 100644
index 000000000..48394158d
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema.rb
@@ -0,0 +1,16 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ class ManyToOneSchema < RelationSchema
+ attr_accessor :foreign_key
+ attr_reader :foreign_key_target
+
+ def initialize(foreign_key:, foreign_key_target:, foreign_collection:)
+ super(foreign_collection, 'ManyToOne')
+ @foreign_key = foreign_key
+ @foreign_key_target = foreign_key_target
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema.rb
new file mode 100644
index 000000000..00cbff603
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema.rb
@@ -0,0 +1,16 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ class OneToManySchema < RelationSchema
+ attr_accessor :origin_key
+ attr_reader :origin_key_target
+
+ def initialize(origin_key:, origin_key_target:, foreign_collection:)
+ super(foreign_collection, 'OneToMany')
+ @origin_key = origin_key
+ @origin_key_target = origin_key_target
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema.rb
new file mode 100644
index 000000000..664a78716
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema.rb
@@ -0,0 +1,16 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ class OneToOneSchema < RelationSchema
+ attr_accessor :origin_key
+ attr_reader :origin_key_target
+
+ def initialize(origin_key:, origin_key_target:, foreign_collection:)
+ super(foreign_collection, 'OneToOne')
+ @origin_key = origin_key
+ @origin_key_target = origin_key_target
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/schema.rb
new file mode 100644
index 000000000..0ec4e044b
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/schema.rb
@@ -0,0 +1,20 @@
+module ForestAdminDatasourceToolkit
+ module Utils
+ class Schema
+ def self.foreign_key?(collection, name)
+ field = collection.fields[name]
+
+ field.type == 'Column' &&
+ collection.fields.any? do |_key, relation|
+ relation.type == 'ManyToOne' && relation.foreign_key == name
+ end
+ end
+
+ def self.primary_key?(collection, name)
+ field = collection.fields[name]
+
+ field.type == 'Column' && field.is_primary_key
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb
new file mode 100644
index 000000000..50bf41ed3
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb
@@ -0,0 +1,3 @@
+module ForestAdminDatasourceToolkit
+ VERSION = "0.1.0"
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit.rbs
new file mode 100644
index 000000000..8410c1789
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit.rbs
@@ -0,0 +1,4 @@
+module ForestAdminDatasourceToolkit
+ VERSION: String
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/collection.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/collection.rbs
new file mode 100644
index 000000000..806e791c0
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/collection.rbs
@@ -0,0 +1,26 @@
+module ForestAdminDatasourceToolkit
+ class Collection
+ attr_reader actions: Hash[string, string]
+ attr_reader charts: Hash[string, string]
+ attr_writer countable: bool
+ attr_reader datasource: Components::Contracts::DatasourceContract
+ attr_reader fields: Hash[string, string]
+ attr_writer fields: void
+ attr_reader name: string
+ attr_reader native_driver: untyped | nil
+ attr_reader schema: Hash[string, string]
+ attr_writer searchable: bool
+ attr_reader segments: Hash[string, string]
+ attr_writer segments: void
+
+ # replace untyped by ColumnSchema|RelationSchema
+ def add_field: (string, untyped) -> void
+
+ # replace untyped by ColumnSchema|RelationSchema
+ def add_fields:(Hash[string, untyped]) -> void
+
+ def is_countable?: -> bool
+
+ def is_searchable?: -> bool
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/components/contracts/collection_contract.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/components/contracts/collection_contract.rbs
new file mode 100644
index 000000000..a89888389
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/components/contracts/collection_contract.rbs
@@ -0,0 +1,29 @@
+module ForestAdminDatasourceToolkit
+ module Components
+ module Contracts
+ class CollectionContract
+ def aggregate: -> untyped
+
+ def create: -> untyped
+
+ def datasource: -> untyped
+
+ def delete: -> untyped
+
+ def execute: -> untyped
+
+ def form: -> untyped
+
+ def list: -> untyped
+
+ def name: -> untyped
+
+ def render_chart: -> untyped
+
+ def schema: -> untyped
+
+ def update: -> untyped
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rbs
new file mode 100644
index 000000000..1bb5c7d3d
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rbs
@@ -0,0 +1,17 @@
+module ForestAdminDatasourceToolkit
+ module Components
+ module Contracts
+ module DatasourceContract
+ def add_collection:(CollectionContract collection) -> untyped
+
+ def charts: -> untyped
+
+ def collection:(string name) -> untyped
+
+ def collections: -> untyped
+
+ def render_chart:(untyped caller, string name) -> untyped
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/datasource.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/datasource.rbs
new file mode 100644
index 000000000..9a03c9ed3
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/datasource.rbs
@@ -0,0 +1,12 @@
+module ForestAdminDatasourceToolkit
+ class Datasource
+ @charts: Hash[string, string]
+ @collections: Hash[string, string]
+
+ def add_collection:(Components::Contracts::CollectionContract collection) -> void
+
+ def collection:(string name) -> Components::Contracts::CollectionContract
+
+ def render_chart:(untyped caller, string name) -> untyped
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/column_schema.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/column_schema.rbs
new file mode 100644
index 000000000..8f7c5d651
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/column_schema.rbs
@@ -0,0 +1,15 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ class ColumnSchema
+ attr_accessor column_type: Array[String] | String
+ attr_reader default_value: untyped
+ attr_reader enum_values: Array[String]
+ attr_accessor filter_operators: Array[String]
+ attr_reader is_primary_key: bool
+ attr_accessor is_read_only: bool
+ attr_accessor is_sortable: bool
+ attr_reader type: String
+ attr_accessor validations: Array[String]
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relation_schema.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relation_schema.rbs
new file mode 100644
index 000000000..7464d67b2
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relation_schema.rbs
@@ -0,0 +1,8 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ class RelationSchema
+ attr_accessor foreign_collection: String
+ attr_reader type: String
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/many_relation_schema.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/many_relation_schema.rbs
new file mode 100644
index 000000000..b12409ee3
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/many_relation_schema.rbs
@@ -0,0 +1,10 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ class ManyRelationSchema
+ attr_accessor foreign_key: String
+ attr_reader foreign_key_target: String
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rbs
new file mode 100644
index 000000000..cc5468814
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rbs
@@ -0,0 +1,11 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ class ManyToManySchema
+ attr_accessor origin_key: String
+ attr_reader origin_key_target: String
+ attr_accessor through_collection: String
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/single_relation_schema.rbs b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/single_relation_schema.rbs
new file mode 100644
index 000000000..b4cb3e63b
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/sig/forest_admin_datasource_toolkit/schema/relations/single_relation_schema.rbs
@@ -0,0 +1,10 @@
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ class SingleRelationSchema
+ attr_accessor origin_key: String
+ attr_reader origin_key_target: String
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/forest_admin_datasource_toolkit_spec.rb b/packages/forest_admin_datasource_toolkit/spec/forest_admin_datasource_toolkit_spec.rb
new file mode 100644
index 000000000..513cefe4f
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/forest_admin_datasource_toolkit_spec.rb
@@ -0,0 +1,5 @@
+RSpec.describe ForestAdminDatasourceToolkit do
+ it 'has a version number' do
+ expect(ForestAdminDatasourceToolkit::VERSION).not_to be_nil
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/collection_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/collection_spec.rb
new file mode 100644
index 000000000..756295bf5
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/collection_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ describe Collection do
+ before do
+ @field = Schema::ColumnSchema.new(column_type: 'String')
+ @datasource = Datasource.new
+ @collection = described_class.new(@datasource, '__collection__')
+ end
+
+ describe 'is_countable' do
+ it 'return false when value is not override' do
+ expect(@collection.is_countable?).to be false
+ end
+
+ it 'return true when value is override' do
+ @collection.countable = true
+ expect(@collection.is_countable?).to be true
+ end
+ end
+
+ describe 'is_searchable' do
+ it 'return false when value is not override' do
+ expect(@collection.is_searchable?).to be false
+ end
+
+ it 'return true when value is override' do
+ @collection.searchable = true
+ expect(@collection.is_searchable?).to be true
+ end
+ end
+
+ describe 'add_field' do
+ it 'raise when a field with the same name already exist' do
+ expect do
+ @collection.add_field('__duplicated__', @field)
+ @collection.add_field('__duplicated__', @field)
+ end.to raise_error(
+ ForestAdminDatasourceToolkit::Exceptions::ForestException,
+ '🌳🌳🌳 Field __duplicated__ already defined in collection'
+ )
+ end
+
+ it 'add field with unique name' do
+ @collection.add_field('__field__', @field)
+
+ expect(@collection.fields).to eq({ '__field__' => @field })
+ end
+ end
+
+ describe 'add_fields' do
+ it 'add all fields with unique name' do
+ @collection.add_fields(
+ {
+ __first__: @field,
+ __second__: @field
+ }
+ )
+
+ expect(@collection.fields).to eq(
+ {
+ __first__: @field,
+ __second__: @field
+ }
+ )
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract_spec.rb
new file mode 100644
index 000000000..d615f8481
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Components
+ module Contracts
+ describe CollectionContract do
+ subject(:collection) { described_class.new }
+
+ it { expect { collection.datasource }.to raise_error(NotImplementedError) }
+ it { expect { collection.schema }.to raise_error(NotImplementedError) }
+ it { expect { collection.name }.to raise_error(NotImplementedError) }
+ it { expect { collection.execute }.to raise_error(NotImplementedError) }
+ it { expect { collection.form }.to raise_error(NotImplementedError) }
+ it { expect { collection.create }.to raise_error(NotImplementedError) }
+ it { expect { collection.list }.to raise_error(NotImplementedError) }
+ it { expect { collection.update }.to raise_error(NotImplementedError) }
+ it { expect { collection.delete }.to raise_error(NotImplementedError) }
+ it { expect { collection.aggregate }.to raise_error(NotImplementedError) }
+ it { expect { collection.render_chart }.to raise_error(NotImplementedError) }
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/datasource_contract_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/datasource_contract_spec.rb
new file mode 100644
index 000000000..8924696aa
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/contracts/datasource_contract_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Components
+ module Contracts
+ describe DatasourceContract do
+ subject(:datasource) { described_class.new }
+
+ it { expect { datasource.collections }.to raise_error(NotImplementedError) }
+ it { expect { datasource.charts }.to raise_error(NotImplementedError) }
+ it { expect { datasource.collection('__foo__') }.to raise_error(NotImplementedError) }
+ it { expect { datasource.add_collection('__collection__') }.to raise_error(NotImplementedError) }
+ it { expect { datasource.render_chart('caller', 'chart') }.to raise_error(NotImplementedError) }
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/datasource_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/datasource_spec.rb
new file mode 100644
index 000000000..772cfbec1
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/datasource_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ describe Datasource do
+ before do
+ @datasource = described_class.new
+ @collection = Collection.new(@datasource, '__collection__')
+ @datasource.add_collection(@collection)
+ end
+
+ it 'expose collections from datasource as an hash' do
+ expect(@datasource.collections).to be_a Hash
+ end
+
+ it 'return an empty hash for charts' do
+ expect(@datasource.charts).to be_a Hash
+ expect(@datasource.charts).to eq({})
+ end
+
+ it 'get collection from datasource' do
+ expect(@datasource.collection('__collection__')).to eq(@collection)
+ end
+
+ it 'raise an error when collection does not exist' do
+ expect do
+ @datasource.collection('__no_such_collection__')
+ end.to raise_error(
+ ForestAdminDatasourceToolkit::Exceptions::ForestException,
+ '🌳🌳🌳 Collection __no_such_collection__ not found.'
+ )
+ end
+
+ it 'raise an error when chart does not exist' do
+ expect do
+ @datasource.render_chart({}, '__no_such_chart__')
+ end.to raise_error(
+ ForestAdminDatasourceToolkit::Exceptions::ForestException,
+ '🌳🌳🌳 No chart named __no_such_chart__ exists on this datasource.'
+ )
+ end
+
+ it 'raise an error when collection already exist' do
+ expect do
+ @datasource.add_collection(@collection)
+ end.to raise_error(
+ ForestAdminDatasourceToolkit::Exceptions::ForestException,
+ '🌳🌳🌳 Collection __collection__ already defined in datasource'
+ )
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/exceptions/forest_exception_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/exceptions/forest_exception_spec.rb
new file mode 100644
index 000000000..b66498bf6
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/exceptions/forest_exception_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Exceptions
+ describe ForestException do
+ subject(:exception) { described_class.new 'message error' }
+
+ it { expect(exception.message).to eq '🌳🌳🌳 message error' }
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/column_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/column_schema_spec.rb
new file mode 100644
index 000000000..69abe8f34
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/column_schema_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Schema
+ describe ColumnSchema do
+ subject(:column) { described_class.new(column_type: 'String') }
+
+ context 'with default values' do
+ it { expect(column.is_primary_key).to be false }
+ it { expect(column.default_value).to be_nil }
+ it { expect(column.is_read_only).to be false }
+ it { expect(column.is_sortable).to be true }
+ it { expect(column.filter_operators).to eq [] }
+ it { expect(column.enum_values).to eq [] }
+ it { expect(column.validations).to eq [] }
+ it { expect(column.column_type).to eq 'String' }
+ end
+
+ context 'when use setters' do
+ before do
+ column.is_read_only = true
+ column.is_sortable = false
+ column.filter_operators = ['Equal']
+ column.validations = ['validation_foo']
+ column.column_type = 'Number'
+ end
+
+ it { expect(column.is_read_only).to be true }
+ it { expect(column.is_sortable).to be false }
+ it { expect(column.filter_operators).to eq ['Equal'] }
+ it { expect(column.validations).to eq ['validation_foo'] }
+ it { expect(column.column_type).to eq 'Number' }
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema_spec.rb
new file mode 100644
index 000000000..ba410904f
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ describe ManyToManySchema do
+ subject(:relation) do
+ described_class.new(
+ foreign_key: 'foreign_key',
+ foreign_key_target: 'foreign_key_target',
+ foreign_collection: 'foreign_collection',
+ through_collection: 'through_collection',
+ origin_key: 'origin_key',
+ origin_key_target: 'origin_key_target'
+ )
+ end
+
+ describe 'getters' do
+ it { expect(relation.type).to eq 'ManyToMany' }
+ it { expect(relation.foreign_key).to eq 'foreign_key' }
+ it { expect(relation.foreign_key_target).to eq 'foreign_key_target' }
+ it { expect(relation.foreign_collection).to eq 'foreign_collection' }
+ it { expect(relation.origin_key).to eq 'origin_key' }
+ it { expect(relation.origin_key_target).to eq 'origin_key_target' }
+ it { expect(relation.through_collection).to eq 'through_collection' }
+ end
+
+ describe 'setters' do
+ before do
+ relation.foreign_key = 'new_foreign_key'
+ relation.origin_key = 'new_origin_key'
+ relation.foreign_collection = 'new_foreign_collection'
+ relation.through_collection = 'new_through_collection'
+ end
+
+ it { expect(relation.foreign_key).to eq 'new_foreign_key' }
+ it { expect(relation.origin_key).to eq 'new_origin_key' }
+ it { expect(relation.foreign_collection).to eq 'new_foreign_collection' }
+ it { expect(relation.through_collection).to eq 'new_through_collection' }
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema_spec.rb
new file mode 100644
index 000000000..d0f180d41
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ describe ManyToOneSchema do
+ subject(:relation) do
+ described_class.new(
+ foreign_key: 'foreign_key',
+ foreign_key_target: 'foreign_key_target',
+ foreign_collection: 'foreign_collection'
+ )
+ end
+
+ describe 'getters' do
+ it { expect(relation.type).to eq 'ManyToOne' }
+ it { expect(relation.foreign_key).to eq 'foreign_key' }
+ it { expect(relation.foreign_key_target).to eq 'foreign_key_target' }
+ it { expect(relation.foreign_collection).to eq 'foreign_collection' }
+ end
+
+ describe 'setters' do
+ before do
+ relation.foreign_key = 'new_foreign_key'
+ relation.foreign_collection = 'new_foreign_collection'
+ end
+
+ it { expect(relation.foreign_key).to eq 'new_foreign_key' }
+ it { expect(relation.foreign_collection).to eq 'new_foreign_collection' }
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema_spec.rb
new file mode 100644
index 000000000..d31b4d818
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ describe OneToManySchema do
+ subject(:relation) do
+ described_class.new(
+ origin_key: 'origin_key',
+ origin_key_target: 'origin_key_target',
+ foreign_collection: 'foreign_collection'
+ )
+ end
+
+ describe 'getters' do
+ it { expect(relation.type).to eq 'OneToMany' }
+ it { expect(relation.origin_key).to eq 'origin_key' }
+ it { expect(relation.origin_key_target).to eq 'origin_key_target' }
+ it { expect(relation.foreign_collection).to eq 'foreign_collection' }
+ end
+
+ describe 'setters' do
+ before do
+ relation.origin_key = 'new_origin_key'
+ relation.foreign_collection = 'new_foreign_collection'
+ end
+
+ it { expect(relation.origin_key).to eq 'new_origin_key' }
+ it { expect(relation.foreign_collection).to eq 'new_foreign_collection' }
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema_spec.rb
new file mode 100644
index 000000000..6cf05653b
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+module ForestAdminDatasourceToolkit
+ module Schema
+ module Relations
+ describe OneToOneSchema do
+ subject(:relation) do
+ described_class.new(
+ origin_key: 'origin_key',
+ origin_key_target: 'origin_key_target',
+ foreign_collection: 'foreign_collection'
+ )
+ end
+
+ describe 'getters' do
+ it { expect(relation.type).to eq 'OneToOne' }
+ it { expect(relation.origin_key).to eq 'origin_key' }
+ it { expect(relation.origin_key_target).to eq 'origin_key_target' }
+ it { expect(relation.foreign_collection).to eq 'foreign_collection' }
+ end
+
+ describe 'setters' do
+ before do
+ relation.origin_key = 'new_origin_key'
+ relation.foreign_collection = 'new_foreign_collection'
+ end
+
+ it { expect(relation.origin_key).to eq 'new_origin_key' }
+ it { expect(relation.foreign_collection).to eq 'new_foreign_collection' }
+ end
+ end
+ end
+ end
+end
diff --git a/packages/forest_admin_datasource_toolkit/spec/spec_helper.rb b/packages/forest_admin_datasource_toolkit/spec/spec_helper.rb
new file mode 100644
index 000000000..410ceae7c
--- /dev/null
+++ b/packages/forest_admin_datasource_toolkit/spec/spec_helper.rb
@@ -0,0 +1,18 @@
+require 'simplecov'
+require 'simplecov_json_formatter'
+require 'simplecov-html'
+require 'forest_admin_datasource_toolkit'
+
+SimpleCov.formatters = [SimpleCov::Formatter::JSONFormatter, SimpleCov::Formatter::HTMLFormatter]
+SimpleCov.start do
+ add_filter 'spec'
+end
+
+RSpec.configure do |config|
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
diff --git a/packages/forest_admin_rails/forest_admin_rails.gemspec b/packages/forest_admin_rails/forest_admin_rails.gemspec
index ab7736845..24582f87d 100644
--- a/packages/forest_admin_rails/forest_admin_rails.gemspec
+++ b/packages/forest_admin_rails/forest_admin_rails.gemspec
@@ -22,9 +22,6 @@ admin work on any Rails application (Rails >= 6.1)."
end
spec.add_dependency "dry-configurable", "~> 1.1"
- # spec.add_dependency "dry-container", "~> 0.11"
- # spec.add_dependency "lightly", "~> 0.4.0"
- # spec.add_dependency "mono_logger", "~> 1.1"
spec.add_dependency "rails", ">= 6.1"
spec.add_dependency "zeitwerk", "~> 2.3"
end
diff --git a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb
index 0372588e6..d1a23c5bc 100644
--- a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb
+++ b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb
@@ -35,6 +35,9 @@ def load_configuration
require Rails.root.join('config', 'forest_admin.rb')
ForestAdminAgent::Builder::AgentFactory.instance.build
+
+ # force eager loading models
+ Rails.application.eager_load!
end
def load_cors