simpleadmin 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,6 +1,4 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
3
-
4
- RSpec::Core::RakeTask.new(:spec)
1
+ # frozen_string_literal: true
5
2
 
3
+ require 'bundler/gem_tasks'
6
4
  task default: :spec
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "simpleadmin"
4
+ require 'bundler/setup'
5
+ require 'simpleadmin'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "simpleadmin"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
@@ -1,7 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/app'
4
+ require 'sequel'
5
+ require 'bcrypt'
6
+
1
7
  require 'simpleadmin/version'
2
- require 'simpleadmin/engine'
8
+ require 'simpleadmin/config'
9
+ require 'simpleadmin/database_connector'
3
10
 
4
- require 'simpleadmin/application'
11
+ # Administrative dashboard without wasting time
12
+ #
13
+ # @since 1.0.0
14
+ #
15
+ # @see https://getsimpleadmin.com
5
16
 
6
17
  module Simpleadmin
18
+ # API endpoints for integration with cloud version of getsimpleadmin.com
19
+ #
20
+ # This is used, Rack conception to connect it in any popular web frameworks
21
+ # like Ruby on Rails, Hanami, Sinatra and etc.
22
+ #
23
+ # @since 1.0.0
24
+ #
25
+ # @example
26
+ #
27
+ # # config/routes.rb
28
+ # Rails.application.routes.draw do
29
+ # mount Simpleadmin::Application, at: 'simpleadmin'
30
+ # end
31
+ class Application < Rack::App
32
+ WIDGETS = %w[quantity week_statistic].freeze
33
+
34
+ before { respond_forbidden! if secret_key_invalid? }
35
+ before { respond_forbidden! unless current_path?('/v1/tables') || allowed_table? }
36
+
37
+ namespace '/v1' do
38
+ get '/widgets/:widget_name' do
39
+ respond_forbidden!(message: 'Nonexistent widget') unless WIDGETS.include?(params['widget_name'])
40
+
41
+ client.public_send(params['widget_name'], params['table_name'])
42
+ end
43
+
44
+ get '/tables' do
45
+ client.tables
46
+ end
47
+
48
+ get '/tables/:table_name' do
49
+ client.table_columns(params['table_name'])
50
+ end
51
+
52
+ get '/resources' do
53
+ client.resources(params.values_at('table_name', 'table_fields', 'per_page',
54
+ 'page', 'query', 'model_attributes', 'sort'))
55
+ end
56
+
57
+ get '/resources/:id' do
58
+ client.resource(params['id'], params['table_name'], params['table_fields'])
59
+ end
60
+
61
+ post '/resources' do
62
+ client.create_resource(request_body_params['table_name'], request_body_params['resource'])
63
+ end
64
+
65
+ patch '/resources/:id' do
66
+ client.update_resource(request_body_params['table_name'], params['id'], request_body_params['resource'])
67
+ end
68
+
69
+ delete '/resources/:id' do
70
+ client.destroy_resouce(request_body_params['table_name'], params['id'])
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def client
77
+ @client ||= connector.client
78
+ end
79
+
80
+ def respond_forbidden!(message: 'Forbidden')
81
+ response.status = 403
82
+ response.write(message)
83
+
84
+ finish!
85
+ end
86
+
87
+ def request_body_params
88
+ @request_body_params ||= Rack::Utils.parse_nested_query(request.body.read)
89
+ end
90
+
91
+ def connector
92
+ @connector ||= DatabaseConnector.new(
93
+ database_credentials: Config.database_credentials
94
+ )
95
+ end
96
+
97
+ def allowed_table?
98
+ Config.allowed_table?(params['table_name'] || request_body_params['table_name'])
99
+ end
100
+
101
+ def secret_key_invalid?
102
+ BCrypt::Password.new(request.get_header('HTTP_SIMPLEADMIN_SECRET_KEY')) != ENV['SIMPLE_ADMIN_SECRET_KEY']
103
+ end
104
+
105
+ def current_path?(path)
106
+ request.path_info == path
107
+ end
108
+ end
7
109
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simpleadmin/decorators/fields_decorator'
4
+
5
+ module Simpleadmin
6
+ module Adapters
7
+ # Base class to provide a unified interface for each adapter
8
+ #
9
+ # @since 1.0.0
10
+ class Base
11
+ def initialize(database_credentials:)
12
+ @database_credentials = database_credentials
13
+ end
14
+
15
+ def tables
16
+ raise NotImplementedError, 'Please follow the unified interface, add method #tables'
17
+ end
18
+
19
+ def table_columns(*_args)
20
+ raise NotImplementedError, 'Please follow the unified interface, add method #table_columns'
21
+ end
22
+
23
+ def resources(*_args)
24
+ raise NotImplementedError, 'Please follow the unified interface, add method #resources'
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :database_credentials
30
+
31
+ def order_asc?(order)
32
+ order == 'asc'
33
+ end
34
+
35
+ def order_desc?(order)
36
+ order == 'desc'
37
+ end
38
+
39
+ def model_class_by_table_name(name)
40
+ name.classify.safe_constantize
41
+ end
42
+
43
+ def table_names
44
+ return client.tables if Config.allowed_tables.include?(:all)
45
+
46
+ Config.allowed_tables
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Simpleadmin
6
+ module Adapters
7
+ # Postgres adapter for API endpoints
8
+ #
9
+ # @since 1.0.0
10
+ class Postgres < Base
11
+ def tables
12
+ table_names.map do |table_name|
13
+ {
14
+ table_name: table_name,
15
+ table_columns: columns_by_table_name(table_name)
16
+ }
17
+ end.to_json
18
+ end
19
+
20
+ def table_columns(table_name)
21
+ columns_by_table_name(table_name).to_json
22
+ end
23
+
24
+ def resources(permitted_params_values)
25
+ table_name, table_fields, per_page, page, query, model_attributes, sort = *permitted_params_values
26
+
27
+ per_page = per_page.to_i
28
+ page = page.to_i
29
+
30
+ table_fields_names = table_fields.map { |field| field['field_name'].to_sym }
31
+
32
+ total = client.from(table_name).count
33
+ resources = client.from(table_name).limit(per_page)
34
+
35
+ resources, total = *search(query, table_name, model_attributes) unless query.nil? || query.empty?
36
+
37
+ resources = resources.offset((per_page * page) - per_page).select(*table_fields_names)
38
+
39
+ if order_asc?(sort['order'])
40
+ resources = resources.order(Sequel.asc(sort['column_name'].to_sym))
41
+ elsif order_desc?(sort['order'])
42
+ resources = resources.order(Sequel.desc(sort['column_name'].to_sym))
43
+ end
44
+
45
+ {
46
+ resources: Decorators::FieldDecorator.call(resources, table_name, table_fields),
47
+ total: total
48
+ }.to_json
49
+ end
50
+
51
+ def resource(resource_id, table_name, table_fields)
52
+ client.from(table_name).first(id: resource_id).slice(*table_fields.map(&:to_sym)).to_json
53
+ end
54
+
55
+ def create_resource(table_name, resource_params)
56
+ Config.on_create.call(model_class_by_table_name(table_name), resource_params)
57
+ end
58
+
59
+ def update_resource(table_name, resource_id, resource_params, _primary_key=:id)
60
+ Config.on_update.call(model_class_by_table_name(table_name), resource_id, resource_params)
61
+ end
62
+
63
+ def destroy_resouce(table_name, resource_id, _primary_key=:id)
64
+ Config.on_destroy.call(model_class_by_table_name(table_name), resource_id)
65
+ end
66
+
67
+ def quantity(table_name)
68
+ {
69
+ widget_name: :quantity,
70
+ result: client.from(table_name).count
71
+ }.to_json
72
+ end
73
+
74
+ def week_statistic(table_name)
75
+ result = 6.downto(0).map do |day|
76
+ current_date_time = DateTime.now - day
77
+
78
+ client.from(table_name).where(
79
+ created_at: beggining_of_day(current_date_time)..at_end_of_day(current_date_time)
80
+ ).count
81
+ end
82
+
83
+ {
84
+ widget_name: :week_statistic,
85
+ result: result
86
+ }.to_json
87
+ end
88
+
89
+ private
90
+
91
+ def client
92
+ @client ||= Sequel.connect(database_credentials)
93
+ end
94
+
95
+ def beggining_of_day(date)
96
+ DateTime.new(date.year, date.month, date.day, 0, 0, 0, 0)
97
+ end
98
+
99
+ def at_end_of_day(date)
100
+ DateTime.new(date.year, date.month, date.day, 23, 59, 59, 59)
101
+ end
102
+
103
+ def columns_by_table_name(name)
104
+ client.schema(name).map do |column_attributes|
105
+ column_name, column_information = *column_attributes
106
+
107
+ {
108
+ column_name: column_name,
109
+ data_type: column_information[:db_type]
110
+ }
111
+ end
112
+ end
113
+
114
+ def search(search_query, table_name, model_attributes)
115
+ query_expressions = model_attributes.map do |model_attribute|
116
+ Sequel.like(model_attribute.to_sym, "%#{search_query}%")
117
+ end
118
+
119
+ query = query_expressions.first if query_expressions.one?
120
+
121
+ query_expressions.each_cons(2) do |current_value, next_value|
122
+ query = query.nil? ? current_value | next_value : query | (current_value | next_value)
123
+ end
124
+
125
+ [
126
+ client.from(table_name).where(query),
127
+ client.from(table_name).where(query).count
128
+ ]
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Simpleadmin
6
+ # Configuration storage to customize allowed tables, to choose a database adapter and name
7
+ #
8
+ # @since 1.0.0
9
+ #
10
+ # @example
11
+ #
12
+ # Simpleadmin::Config.setup do |config|
13
+ # config.database_credentials = {
14
+ # adapter: :postgres,
15
+ # database: 'squiz_development'
16
+ # }
17
+ #
18
+ # config.allowed_tables = ['users'] # Allowed tables
19
+ # # config.allowed_tables = [:all] # Allow all tables
20
+ #
21
+ # config.on_create = lambda do |model_class, resource_params|
22
+ # model_class.create(resource_params)
23
+ # end
24
+ #
25
+ # config.on_update = lambda do |model_class, resource_id, resource_params|
26
+ # model_class.find(resource_id).update(resource_params)
27
+ # end
28
+ #
29
+ # config.on_destroy = lambda do |model_class, resource_id|
30
+ # model_class.find(resource_id).destroy
31
+ # end
32
+ # end
33
+ class Config
34
+ DEFAULT_TABLE_SCHEMAS = ['public'].freeze
35
+
36
+ include Singleton
37
+
38
+ attr_accessor :table_schemas, :database_credentials, :allowed_tables, :client
39
+ attr_accessor :on_create, :on_update, :on_destroy
40
+
41
+ class << self
42
+ def setup
43
+ yield(instance)
44
+ end
45
+
46
+ def allowed_table?(table_name)
47
+ return true if instance.allowed_tables.include?(:all)
48
+
49
+ instance.allowed_tables.include?(table_name)
50
+ end
51
+
52
+ def allowed_tables
53
+ instance.allowed_tables || []
54
+ end
55
+
56
+ def database_credentials
57
+ instance.database_credentials
58
+ end
59
+
60
+ def on_create
61
+ raise NotImplementedError, 'Please define #on_create in an initializer to use gem' if instance.on_create.nil?
62
+
63
+ instance.on_create
64
+ end
65
+
66
+ def on_update
67
+ raise NotImplementedError, 'Please define #on_update in an initializer to use gem' if instance.on_update.nil?
68
+
69
+ instance.on_update
70
+ end
71
+
72
+ def on_destroy
73
+ raise NotImplementedError, 'Please define #on_destroy in an initializer to use gem' if instance.on_destroy.nil?
74
+
75
+ instance.on_destroy
76
+ end
77
+
78
+ def table_schemas
79
+ instance.table_schemas || DEFAULT_TABLE_SCHEMAS
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simpleadmin/adapters/base'
4
+ require 'simpleadmin/adapters/postgres'
5
+
6
+ module Simpleadmin
7
+ # Connector service that handles different databases to provide unified API endpoints
8
+ #
9
+ # @since 1.0.0
10
+ #
11
+ class DatabaseConnector
12
+ ADAPTERS_MAPPER = {
13
+ postgres: Adapters::Postgres
14
+ }.freeze
15
+
16
+ def initialize(database_credentials:)
17
+ @database_credentials = database_credentials
18
+ end
19
+
20
+ def client
21
+ adapter.new(database_credentials: database_credentials)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :database_credentials
27
+
28
+ def adapter
29
+ adapter_class = ADAPTERS_MAPPER[database_credentials[:adapter]]
30
+ raise ArgumentError, 'Invalid adapter name or adapter not exist' if adapter_class.nil?
31
+
32
+ adapter_class
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Simpleadmin
4
+ module Decorators
5
+ module Fields
6
+ class Base
7
+ def initialize(table_name, table_field_name, resource)
8
+ @table_name = table_name
9
+ @table_field_name = table_field_name
10
+
11
+ @resource = resource
12
+ end
13
+
14
+ def call
15
+ raise NotImplementedError, 'Please follow the unified interface, add method #call'
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :table_name, :table_field_name, :resource
21
+
22
+ def model
23
+ model_class = table_name.classify.safe_constantize
24
+
25
+ if model_class.nil?
26
+ raise ArgumentError, "The model (#{table_name.classify}) does not exist"
27
+ else
28
+ model_class
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end