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